Page Menu
Home
GRNET
Search
Configure Global Search
Log In
Files
F886121
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
Fri, Aug 8, 8:44 PM
Size
37 KB
Mime Type
text/x-diff
Expires
Sun, Aug 10, 8:44 PM (23 h, 14 m)
Engine
blob
Format
Raw Data
Handle
246070
Attached To
rARCHIVING archiving
View Options
diff --git a/app/controllers/clients_controller.rb b/app/controllers/clients_controller.rb
index 2111e79..70f5f8b 100644
--- a/app/controllers/clients_controller.rb
+++ b/app/controllers/clients_controller.rb
@@ -1,174 +1,188 @@
class ClientsController < ApplicationController
before_action :require_logged_in
before_action :fetch_client, only: [:show, :jobs, :logs, :stats, :users, :restore, :run_restore,
:restore_selected, :remove_user]
before_action :fetch_logs, only: [:logs]
before_action :require_non_blocked_client, only: [:restore, :restore_selected, :run_restore]
# 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
if @client.manually_inserted?
@invitation = @client.host.invitations.new
excluded_ids = @users.pluck(:id) + @client.host.invitations.pluck(:user_id)
@available_users = User.where(enabled: true).institutional.
where.not(id: excluded_ids).pluck(:username, :id)
end
end
# DELETE /clients/1/user
def remove_user
user = @client.host.users.find(params[:user_id])
redirect_path = users_client_path(@client)
if @client.host.users.count == 1
flash[:alert] = 'You can not remove the last user'
elsif @client.host.users.delete(user)
flash[:success] =
if @client.manually_inserted?
'User successfully removed'
else
'User must be removed from the VM\'s list from your VM provider too (ViMa or Okeanos).'
end
if user.id == current_user.id
redirect_path = clients_path
end
else
flash[:alert] = 'User not removed, something went wrong'
end
redirect_to redirect_path
end
# GET /clients/1/restore
def restore
+ @restore_clients = Client.for_user(current_user.id)
+
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]
restore_point = fetch_restore_point
+ restore_client = fetch_restore_client
if params[:commit] == 'Restore All Files'
- if @location.nil? || fileset.nil? || !@client.host.restore(fileset, @location, restore_point)
+ if @location.nil? || fileset.nil? ||
+ !@client.host.restore(fileset, @location, restore_point, restore_client)
flash[:error] = 'Something went wrong, try again later'
else
- flash[:success] =
- "Restore job issued successfully, files will be soon available in #{@location}"
+ msg = "Restore job issued successfully, files will be soon available in #{@location}"
+ msg << " of client #{restore_client}" if restore_client.present?
+ flash[:success] = msg
end
render js: "window.location = '#{client_path(@client)}'"
else
session[:job_ids] = @client.get_job_ids(fileset, restore_point)
+ session[:restore_client] = restore_client
Bvfs.new(@client, session[:job_ids]).update_cache
render 'select_files'
end
end
# POST /clients/1/restore_selected
def restore_selected
- Bvfs.new(@client, session[:job_ids]).restore_selected_files(params[:files], params[:location])
+ Bvfs.new(@client, session[:job_ids]).
+ restore_selected_files(params[:files], params[:location], nil, session[:restore_client])
session.delete(:job_ids)
+ session.delete(:restore_client)
flash[:success] =
"Restore job issued successfully, files will be soon available in #{params[:location]}"
redirect_to client_path(@client)
end
# GET /clients/1/tree?id=1
def tree
@client = Client.for_user(current_user.id).find(params[:client_id])
bvfs = Bvfs.new(@client, session[:job_ids])
pathid = params[:id].to_i
if pathid.nonzero?
bvfs.fetch_dirs(pathid)
else
bvfs.fetch_dirs
end
tree = bvfs.extract_dir_id_and_name.map do |id, name|
{ id: id, text: name, state: { checkbox_disabled: true }, children: true }
end
if pathid.nonzero?
bvfs.fetch_files(pathid)
bvfs.extract_file_id_and_name.each do |id, name|
tree << { id: id, text: name, type: 'file' }
end
end
render json: tree
end
private
def require_non_blocked_client
if @client.host.blocked?
flash[:error] = 'Client disabled by admins'
redirect_to clients_path
end
end
def fetch_client
@client = Client.for_user(current_user.id).find(params[:id])
@client_ids = [@client.id]
end
+ def fetch_restore_client
+ if params[:restore_client]
+ Client.for_user(current_user.id).find_by(ClientId: params[:restore_client]).try(:name)
+ end
+ end
+
def fetch_jobs_info
@stats = JobStats.new(@client_ids)
end
def get_charts
@job_status = ChartGenerator.job_statuses(@client_ids, days_ago)
@job_stats = ChartGenerator.job_stats(@client_ids, days_ago - 1)
end
def fetch_restore_point
if params['restore_time(4i)'].blank? || params['restore_time(5i)'].blank? ||
params[:restore_date].blank?
return nil
end
restore_point =
"#{params[:restore_date]} #{params['restore_time(4i)']}:#{params['restore_time(5i)']}:00"
begin
DateTime.strptime(restore_point, '%Y-%m-%d %H:%M:%S')
rescue
return nil
end
restore_point
end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 467f9b8..e9f8477 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,174 +1,192 @@
module ApplicationHelper
# Custom helper for better display of big numbers
# @example number_by_magnitude(4242)
# "4.2K"
#
# @param number[Numeric]
# @return [String] human friendly respresentation
def number_by_magnitude(number)
number_to_human(number, units: { thousand: :K, million: :M, billion: :G })
end
# Creates a bootstrap form-group div with an additional 'Add' button next to the select field
#
# @param object[ActiveRecord::Object] the form's subject
# @param resource[Symbol] the objects class
# @param attr[Symbol] the select box's attribute
# @param attr_name[String] the attribute's display name
# @param options[Array] the select box options
# @param path[String] the add button's path
def select_with_errors_and_button(object, resource, attr, attr_name, options, path)
has_errors = object.errors[attr].present?
content_tag(:div, class: "form-group #{' has-error' if has_errors }") do
attr_label = label(resource, attr, attr_name, class: 'control-label col-xs-5 required')
select_div = content_tag(:div, class: 'col-xs-5') do
select_part = select_tag([resource, attr].join('_').to_sym,
options,
include_blank: true,
name: "#{resource}[#{attr}]",
class: 'form-control'
)
if has_errors
select_part.concat(content_tag(:span, class: 'help-block') { object.errors[attr].first })
end
select_part
end
button_part = content_tag(:div, class: 'col-xs-1') do
link_to path do
content_tag(:span, class: 'glyphicon glyphicon-plus text-success') {}
end
end
attr_label.concat(select_div).concat(button_part)
end
end
# Returns a style class depending on the given parameter
#
# @param status[Char]
def success_class(status)
case status
when 'T' then 'success'
when 'E' then 'danger'
when 'f' then 'fatal'
end
end
# Fetches the html class for a given path
#
# @param path[String] the path to check for
# @param partial[Boolean] forces a left partial match
#
# @return [Hash] { class: 'active' } if the given path is the current page
def active_class(path, partial = false)
if current_page?(path) || (partial && request.path.starts_with?(path))
{ class: 'active' }
else
{}
end
end
# Constructs a breadcrumb out the given options
#
# @param options[Hash] a hash containing the breadcrumb links in name: path sets
# @return an html ol breadcrumb
def breadcrumb_with(options)
content_tag(:ol, class: 'breadcrumb') do
options.map { |name, path|
content_tag(:li, active_class(path)) do
link_to name, path
end
}.inject { |result, element| result.concat(element) }
end
end
# Constructs a list with the given array elements
#
# @example:
# inline_list([:foo, :bar])
#
# <ul class="list-inline'>
# <li><span class="label label-default">foo</span></li>
# <li><span class="label label-default">bar</span></li>
# </ul>
#
# @param arr[Array]
# @return an html ul list
def inline_list(arr)
content_tag(:ul, class: 'list-inline') do
arr.map { |element|
content_tag(:li) do
content_tag(:span, class: 'label label-default') do
element
end
end
}.inject { |result, element| result.concat(element) }
end
end
# Generates a span with a yes or no and the corresponding formatting
# according to the value's falseness
#
# @param value[Integer]
def yes_no(value)
klass = value == 1 ? 'label label-success' : 'label label-danger'
text = value == 1 ? 'yes' : 'no'
content_tag(:span, class: klass) { text }
end
# Generates a percentage and adds some color coding for better visibility
#
# @param ratio [Numeric] the ratio
# @param quota [Integer] the client's space quota
#
# @return [String] an html label tag
def pretty_percentage(ratio, quota)
color = ([[ratio, 0.2].max, 0.99].min * 256).to_i.to_s(16) << '0000'
content_tag(:label, class: 'label', style: "background-color:##{color}") do
number_to_percentage(100 * ratio, precision: 1)
end
end
# Generates a button that may be disabled
#
# @param disabled[Boolean]
# @param display_text[String]
# @param url[String]
# @param opts[Hash]
def button_or_disabled_with_label(disabled, display_text, url, opts = {})
icon_class = opts.delete(:icon_class)
text_class = opts.delete(:text_class)
if disabled
url = '#'
opts.merge!(disabled: true)
opts.reverse_merge!(title: 'Client is blocked')
opts.delete(:method)
else
opts.delete(:title)
end
link_to url, opts do
[
content_tag(:label, class: [icon_class, text_class].join(' ')) { },
display_text
].join(' ').html_safe
end
end
# Generates a span that contains a text and a questionmark label.
# hovering on that questionmark will display a helper text
#
# @param text[String] the displayed text
# @param tooltip[String] the displayed helper text
def tooltip_label(text, tooltip)
content_tag(:span, class: "data-toggle", title: tooltip) do
[
text,
content_tag(:label, class: "glyphicon glyphicon-question-sign") { }
].join(' ').html_safe
end
end
+
+ # Generate a div that contains a helper text that is properly aligned with a form
+ #
+ # @param text[String] the displayed text
+ # @param label_class[String] the form's labe_col
+ # @param control_class[String] the form's control_col
+ def help_block(text, label_class, control_class)
+ content_tag(:div, class: 'form-group') do
+ [
+ content_tag(:label, class: "#{label_class} control-label") { },
+ content_tag(:div, class: control_class) do
+ content_tag(:p, class: 'form-control-static help-block') do
+ text
+ end
+ end
+ ].join(' ').html_safe
+ end
+ end
end
diff --git a/app/models/host.rb b/app/models/host.rb
index 07897ac..b54f03b 100644
--- a/app/models/host.rb
+++ b/app/models/host.rb
@@ -1,389 +1,390 @@
# 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
}
# The default file daemon port
DEFAULT_PORT = 9102
enum origin: { institutional: 0, vima: 1, okeanos: 2 }
serialize :email_recipients, JSON
has_many :simple_configurations
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, :quota, numericality: { greater_than: 0 }
validates :fqdn, presence: true, uniqueness: true
validate :fqdn_format
validate :valid_recipients
scope :not_baculized, -> {
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, :set_password, :set_port, :set_quota
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 [:configured, :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
# API serializer
# Override `as_json` method to personalize for API use.
def as_json(opts={})
if for_api = opts.delete(:for_api)
api_json
else
super(opts)
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
# Determines if a host needs a simple config
#
# @return [Boolean]
def needs_simple_config?
job_templates.none? && simple_configurations.none?
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)
+ # @param restore_client[String]] the desired restore client
+ def restore(file_set_id, location, restore_point=nil, restore_client=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)
+ bacula_handler.restore(job_ids, file_set_name, restore_point, location, restore_client)
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, self).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
# Resets the hosts token
#
# @return [Boolean]
def recalculate_token
self.password = token
save
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
def set_password
return true if persisted?
self.password = token
end
def token
Digest::SHA256.hexdigest(
Time.now.to_s + Rails.application.secrets.salt + fqdn.to_s
)
end
def set_port
return true if persisted?
self.port = DEFAULT_PORT
end
def set_quota
return true if persisted?
self.quota = ConfigurationSetting.client_quota
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
def valid_recipients
if !email_recipients.all? { |email| email =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
self.errors.add(:email_recipients)
end
end
# Handles the returned attribues for api
#
# @return [Hash] of the desired attributes for api use
def api_json
{
id: id,
name: name,
uname: client.uname,
port: port,
file_retention: "#{file_retention} #{file_retention_period_type}",
job_retention: "#{job_retention} #{job_retention_period_type}",
quota: quota,
last_backup: client.last_job_datetime,
files: client.files_count,
space_used: client.backup_jobs_size,
collaborators: email_recipients,
backup_jobs: job_templates.enabled.backup.map(&:api_json),
restorable_filesets: client.file_sets.map(&:api_json)
}
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/views/clients/restore.html.erb b/app/views/clients/restore.html.erb
index 11b5e13..0c8d37b 100644
--- a/app/views/clients/restore.html.erb
+++ b/app/views/clients/restore.html.erb
@@ -1,65 +1,74 @@
<div class="row">
<div class="col-xs-4">
<% if @client.is_backed_up? %>
<div class="panel panel-default">
<div class="panel-heading">
<h3>Restore files for "<%= @client.name %>"</h3>
</div>
- <br />
+ <div class="panel-body">
<%= bootstrap_form_tag(url: run_restore_client_path(@client), remote: true,
layout: :horizontal, label_col: 'col-xs-4', control_col: 'col-xs-7',
html: { id: 'basic-form' } ) do |f| %>
- <div class="form-group">
- <label class="col-xs-4 control-label"></label>
- <div class="col-xs-7">
- <p class="form-control-static">
- Restore to most recent backup by leaving date and time blank
- </p>
- </div>
- </div>
+ <%= help_block('Restore to most recent backup by leaving date and time blank',
+ 'col-xs-4', 'col-xs-7') %>
<%= f.text_field :restore_date %>
<%= f.time_select :restore_time, ignore_date: true, minute_step: 30, prompt: true %>
<%= f.select(:fileset,
options_from_collection_for_select(@client.file_sets, :id, :file_set)) %>
<%= f.text_field :restore_location, placeholder: '/tmp/default_restore' %>
+ <hr />
+
+ <%= help_block(
+ 'Restore and backup clients <strong>must</strong> have the same encryption key'.html_safe,
+ 'col-xs-4', 'col-xs-7') %>
+
+ <%= f.select(
+ :restore_client,
+ options_from_collection_for_select(@restore_clients,
+ :id, :name, @client.id),
+ label: tooltip_label('Restore Client',
+ 'Client where the backup will be restored to')) %>
+
+ <hr />
+
<div class="row">
<div class="col-xs-5 col-xs-offset-6 text-right">
<%= f.submit 'Select Specific Files', id: 'select-files', class: 'btn btn-primary' %>
</div>
</div>
<br />
<div class="row">
<div class="col-xs-3 col-xs-offset-8">
<%= f.submit 'Restore All Files', class: 'btn btn-warning text-right',
data: { confirm: "This will restore all your files" }
%>
</div>
</div>
<% end %>
- </br>
+ </div>
</div>
<% else %>
<div class="alert alert-warning">
<p>Can not issue a restore for this client. It does not have any successful backups</p>
</div>
<% end %>
<%= link_to 'Back to client', client_path(@client), class: 'btn btn-danger', role: 'button' %>
</div>
<%= render partial: 'file_selector' %>
</div>
<script type="text/javascript">
<% min_date, max_date = @client.backup_enabled_datetime_range %>
var minDate = '<%= min_date %>';
var maxDate = '<%= max_date %>';
$('#restore_date').datepicker({
dateFormat: 'yy-mm-dd',
minDate: minDate,
maxDate: maxDate
});
</script>
diff --git a/lib/bacula_handler.rb b/lib/bacula_handler.rb
index c111566..5c4e13e 100644
--- a/lib/bacula_handler.rb
+++ b/lib/bacula_handler.rb
@@ -1,201 +1,205 @@
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.bacula_ready?
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 level=full 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 restore_point[String] the restore datetime
# @param location[String] the desired restore location
- def restore(job_ids, file_set_name, restore_point, location="/tmp/bacula-restore")
+ # @param restore_client[String] the desired restore client
+ def restore(job_ids, file_set_name, restore_point, location="/tmp/bacula-restore", restore_client = nil)
command = "echo \"restore client=\\\"#{host.name}\\\" "
+ if restore_client && restore_client != host.name
+ command << "restoreclient=\\\"#{restore_client}\\\" "
+ end
command << "jobid=#{job_ids.join(',')} "
command << "where=\\\"#{location}\\\" "
command << "fileset=\\\"#{file_set_name}\\\" "
if restore_point
command << "before=\\\"#{restore_point}\\\" "
else
command << "current "
end
command << "select all done yes\" "
command << "| #{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/lib/bvfs.rb b/lib/bvfs.rb
index 6c36d1e..9abe3cc 100644
--- a/lib/bvfs.rb
+++ b/lib/bvfs.rb
@@ -1,132 +1,139 @@
class Bvfs
attr_accessor :client, :jobids, :files, :dirs
def initialize(client, jobids)
@jobids = jobids.join(',')
@client = client
end
# Fetches the directories that exist in the given directory and
# stores the output to the result instance variable
#
# @param pathid[String|Integer] If nil or omitted, root directory is implied
def fetch_dirs(pathid=nil)
path = pathid.nil? ? 'path=\"\"':"pathid=#{pathid}"
command = pipe_to_bconsole(".bvfs_lsdirs jobid=#{jobids} #{path}")
@dirs = exec_command(command)
end
# Fetches the files that exist in the given directory and
# stores the output to the result instance variable
#
# @param pathid[String|Integer] If nil or omitted, root directory is implied
def fetch_files(pathid=nil)
path = pathid.nil? ? 'path=\"\"':"pathid=#{pathid}"
command = pipe_to_bconsole(".bvfs_lsfiles jobid=#{jobids} #{path}")
@files = exec_command(command)
end
# Extracts the id and name of the bvfs_lsdirs command
def extract_dir_id_and_name
dirs.
split("\n").
select { |x| x[/^(\d+\W){4}.*[^.]/] }.
map {|x| s = x.split("\t"); [s.first, s.last.gsub(/(.)\/$/, '\\1')] }.
select { |x| !['.', '..'].include?(x.last) }.
to_h
end
# Extracts the id and name of the bvfs_lsfiles command
def extract_file_id_and_name
files.
split("\n").
select { |x| x[/^(\d+\W){4}.*[^.]/] }.
map {|x| s = x.split("\t"); [s.third, s.last] }.
select { |x| !['.', '..'].include?(x.last) }.
to_h
end
# Updates the bvfs cache for the specific job ids.
# This can take some time. Always provide a job id.
def update_cache
command = pipe_to_bconsole(".bvfs_update jobid=#{jobids}")
exec_command(command)
end
# Handles restore of multiple selected files and directories
#
# * creates a db table with the needed files
# * issues the restore
# * cleans up the table
#
# @param file_ids[Array] the file ids that will be restored
# @param location[String] the client's restore location
# @param dir_ids[Array] the directory ids that will be restored
- def restore_selected_files(file_ids, location = nil, dir_ids = nil)
+ # @param restore_client[String] the client where the restore will be sent
+ def restore_selected_files(file_ids, location = nil, dir_ids = nil, restore_client = nil)
location ||= '/tmp/bacula_restore/'
dir_ids ||= []
dbname = "b2#{client.id}#{(Time.now.to_f * 100).to_i}"
shell_command = [
create_restore_db(dbname, file_ids, dir_ids),
- restore_command(dbname, location),
+ restore_command(dbname, location, restore_client),
clear_cache
].map { |command| pipe_to_bconsole(command) }.join(' && ')
Rails.logger.warn("[BVFS]: #{shell_command}")
pid = spawn shell_command
Process.detach(pid)
end
# Issues the bvfs command for cleaning up a temporary db table
#
# @param dbname[String] the database table's name
def purge_db(dbname)
exec_command(pipe_to_bconsole(".bvfs_cleanup path=#{dbname}"))
end
private
# Generates the bvfs command needed in order to create a temporary database
# that will hold the files that we want to restore.
#
# @param file_ids[Array] the file ids that will be restored
# @param dir_ids[Array] the directory ids that will be restored
#
# @return [String] bvfs restore command
def create_restore_db(dbname, file_ids, dir_ids)
params = "jobid=#{jobids} path=#{dbname}"
params << " fileid=#{file_ids.join(',')}" if file_ids.any?
params << " dirid=#{dir_ids.join(',')}" if dir_ids.any?
".bvfs_restore #{params}"
end
# Generates the restore command
#
# @param dbname[String] the name of the db table that has the desired files
# @param location[String] the client's restore location
+ # @param restore_client[String] the client that will receive the restored files
#
# @return [String] bconsole's restore command
- def restore_command(dbname, location)
- "restore file=?#{dbname} client=\\\"#{client.name}\\\" where=\\\"#{location}\\\" yes"
+ def restore_command(dbname, location, restore_client)
+ command = "restore file=?#{dbname} client=\\\"#{client.name}\\\" where=\\\"#{location}\\\" "
+ if restore_client && restore_client != client.name
+ command << "restoreclient=\\\"#{restore_client}\\\" "
+ end
+ command << "yes"
+ command
end
def clear_cache
'.bvfs_clear_cache yes'
end
def exec_command(command)
Rails.logger.warn("[BVFS]: #{command}")
`#{command}`
end
def pipe_to_bconsole(what)
"echo \"#{what}\" | #{bconsole}"
end
def bconsole
"bconsole -c #{Rails.root}/config/bconsole.conf"
end
end
Event Timeline
Log In to Comment