diff --git a/app/controllers/admin/pools_controller.rb b/app/controllers/admin/pools_controller.rb
new file mode 100644
index 0000000..dadb7c0
--- /dev/null
+++ b/app/controllers/admin/pools_controller.rb
@@ -0,0 +1,58 @@
+class Admin::PoolsController < Admin::BaseController
+ before_action :fetch_pool, only: [:show, :edit, :update]
+
+ # GET /admin/pools
+ def index
+ @pools = Pool.all
+ end
+
+ # GET /admin/pools/new
+ def new
+ @pool = Pool.new
+ end
+
+ # GET /admin/pools/:id/edit
+ def edit; end
+
+ # GET /admin/pools/:id
+ def show; end
+
+ # POST /admin/pools
+ def create
+ @pool = Pool.new(fetch_params)
+
+ if @pool.submit_to_bacula
+ flash[:success] = 'Pool created succesfully'
+ redirect_to admin_pools_path
+ else
+ flash[:alert] = 'Pool not created'
+ render :new
+ end
+ end
+
+ # PATCH /admin/pools/:id
+ def update
+ if @pool.update_attributes(fetch_params)
+ flash[:success] = 'Pool updated succesfully'
+ redirect_to admin_pools_path
+ else
+ flash[:alert] = 'Pool not updated'
+ render :edit
+ end
+ end
+
+ private
+
+ def fetch_pool
+ @pool = Pool.find(params[:id])
+ end
+
+ def fetch_params
+ params.require(:pool).permit(
+ [
+ :name, :name_confirmation, :vol_retention, :use_once, :auto_prune, :recycle,
+ :max_vols, :max_vol_jobs, :max_vol_files, :max_vol_bytes, :label_format
+ ]
+ )
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 86124f0..1fdf26a 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -1,109 +1,119 @@
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_if !current_page?(path), name, path
end
}.inject { |result, element| result.concat(element) }
end
end
# Constructs a list with the given array elements
#
# @example:
# inline_list([:foo, :bar])
#
#
foo
#
bar
#
#
# @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
end
diff --git a/app/models/pool.rb b/app/models/pool.rb
index 8061ea9..8767743 100644
--- a/app/models/pool.rb
+++ b/app/models/pool.rb
@@ -1,45 +1,116 @@
# Bacula Pool
#
# The Pool table contains one entry for each media pool controlled by Bacula in
# this database. One media record exists for each of the NumVols contained in the Pool.
# The PoolType is a Bacula defined keyword.
# The MediaType is defined by the administrator, and corresponds to the MediaType
# specified in the Director's Storage definition record.
# The CurrentVol is the sequence number of the Media record for the current volume.
class Pool < ActiveRecord::Base
self.table_name = :Pool
self.primary_key = :PoolId
alias_attribute :pool_id, :PoolId
alias_attribute :name, :Name
alias_attribute :num_vols, :NumVols
alias_attribute :max_vols, :MaxVols
alias_attribute :use_once, :UseOnce
alias_attribute :use_catalog, :UseCatalog
alias_attribute :accept_any_volume, :AcceptAnyVolume
alias_attribute :vol_retention, :VolRetention
alias_attribute :vol_use_duration, :VolUseDuration
alias_attribute :max_vol_jobs, :MaxVolJobs
alias_attribute :max_vol_files, :MaxVolFiles
alias_attribute :max_vol_bytes, :MaxVolBytes
alias_attribute :auto_prune, :AutoPrune
alias_attribute :recycle, :Recycle
alias_attribute :action_on_purge, :ActionOnPurge
alias_attribute :pool_type, :PoolType
alias_attribute :label_type, :LabelType
alias_attribute :label_format, :LabelFormat
alias_attribute :enabled, :Enabled
alias_attribute :scratch_pool_id, :ScratchPoolId
alias_attribute :recycle_pool_id, :RecyclePoolId
alias_attribute :next_pool_id, :NextPoolId
alias_attribute :migration_high_bytes, :MigrationHighBytes
alias_attribute :migration_low_bytes, :MigrationLowBytes
alias_attribute :migration_time, :MigrationTime
has_many :jobs, foreign_key: :PoolId
has_many :media, foreign_key: :PoolId
+ validates_confirmation_of :name
+
+ BOOLEAN_OPTIONS = [['no', 0], ['yes', 1]]
+ POOL_OPTIONS = [:name, :vol_retention, :use_once, :auto_prune, :recycle,
+ :max_vols, :max_vol_jobs, :max_vol_files, :max_vol_bytes, :label_format]
+
+ # @return [Array] of all the available pools by name
def self.available_options
pluck(:Name)
end
+
+ # Persists the unpersisted record to bacula via its bacula handler
+ #
+ # @return [Boolean] according to the persist status
+ def submit_to_bacula
+ return false if !valid? || !ready_for_bacula?
+ sanitize_names
+ bacula_handler.deploy_config
+ end
+
+ # Constructs an array where each element is a line for the Job's bacula config
+ #
+ # @return [Array]
+ def to_bacula_config_array
+ ['Pool {'] +
+ options_array.map { |x| " #{x}" } +
+ ['}']
+ end
+
+ # Human readable volume retention
+ #
+ # @return [String] the volume's retention in days
+ def vol_retention_human
+ "#{vol_retention_days} days"
+ end
+
+ # volume retention in days
+ def vol_retention_days
+ vol_retention / 1.day.to_i
+ end
+
+ private
+
+ # proxy object for bacula pool handling
+ def bacula_handler
+ BaculaPoolHandler.new(self)
+ end
+
+ # pool names and label formats should only contain alphanumeric values
+ def sanitize_names
+ self.name = name.gsub(/[^a-zA-Z0-9]/, '_')
+ self.label_format = label_format.gsub(/[^a-zA-Z0-9]/, '_')
+ end
+
+ def options_array
+ boolean_options = Hash[BOOLEAN_OPTIONS].invert
+ [
+ "Name = \"#{name}\"",
+ "Volume Retention = #{vol_retention_human}",
+ "Use Volume Once = #{boolean_options[use_once.to_i]}",
+ "AutoPrune = #{boolean_options[auto_prune.to_i]}",
+ "Recycle = #{boolean_options[recycle.to_i]}",
+ "Maximum Volumes = #{max_vols}",
+ "Maximum Volume Jobs = #{max_vol_jobs}",
+ "Maximum Volume Files = #{max_vol_files}",
+ "Maximum Volume Bytes = #{max_vol_bytes}G",
+ "Label Format = \"#{label_format}\"",
+ "Pool Type = Backup"
+ ]
+ end
+
+ def ready_for_bacula?
+ POOL_OPTIONS.all? { |attr| self.send(attr).present? }
+ end
end
diff --git a/app/views/admin/pools/_form.html.erb b/app/views/admin/pools/_form.html.erb
new file mode 100644
index 0000000..6450261
--- /dev/null
+++ b/app/views/admin/pools/_form.html.erb
@@ -0,0 +1,25 @@
+<%= bootstrap_form_for(@pool, url: admin_pools_path, method: :post, layout: :horizontal,
+ label_col: 'col-xs-5', control_col: 'col-xs-6') do |f| %>
+ <%= f.text_field :name, required: true %>
+ <%= f.text_field :name_confirmation, required: true %>
+ <%= f.number_field :vol_retention, label: 'Volume Retention in days',
+ value: @pool.vol_retention_days || 180, required: true %>
+ <%= f.select :use_once, options_for_select(Pool::BOOLEAN_OPTIONS), required: true %>
+ <%= f.select :auto_prune, options_for_select(Pool::BOOLEAN_OPTIONS.reverse),
+ required: true %>
+ <%= f.select :recycle, options_for_select(Pool::BOOLEAN_OPTIONS.reverse),
+ required: true %>
+ <%= f.number_field :max_vols, required: true, value: 0 %>
+ <%= f.number_field :max_vol_jobs, min: 0, required: true %>
+ <%= f.number_field :max_vol_files, min: 0, required: true %>
+ <%= f.number_field :max_vol_bytes, label: 'Max Volume GB', min: 0, required: true %>
+ <%= f.text_field :label_format, required: true %>
+
+
diff --git a/app/views/admin/pools/new.html.erb b/app/views/admin/pools/new.html.erb
new file mode 100644
index 0000000..67d7b43
--- /dev/null
+++ b/app/views/admin/pools/new.html.erb
@@ -0,0 +1,11 @@
+
+
+
+
+
New Pool
+
+ <%= render partial: 'form' %>
+
+
+
+
diff --git a/app/views/shared/_nav.html.erb b/app/views/shared/_nav.html.erb
index bfacf33..ae04748 100644
--- a/app/views/shared/_nav.html.erb
+++ b/app/views/shared/_nav.html.erb
@@ -1,76 +1,79 @@
diff --git a/config/routes.rb b/config/routes.rb
index 34d5ee1..d652ea1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,91 +1,93 @@
Rails.application.routes.draw do
root 'application#index'
post 'grnet' => 'application#grnet'
get 'institutional' => 'application#institutional'
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
post :restore_selected
end
collection do
post :index
end
end
resources :clients, only: [], param: :client_id do
member do
get :tree
end
end
resources :hosts, only: [:new, :create, :show, :edit, :update, :destroy] do
member do
post :submit_config
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, :edit, :update, :destroy]
resources :schedules, only: [:show, :new, :edit, :create, :update, :destroy]
end
namespace :admin do
match '/', to: 'base#index', via: [:get, :post]
get '/login' => 'base#login', as: :login
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
post :block
post :unblock
delete :revoke
end
end
resources :hosts, only: [:show] do
collection do
get :unverified
end
member do
post :verify
end
end
resources :users, only: [:index, :new, :create, :show, :edit, :update] do
member do
patch :ban
patch :unban
end
end
+
+ resources :pools, only: [:index, :new, :create, :show, :edit, :update]
end
end
diff --git a/spec/routing/admin/pools_routing_spec.rb b/spec/routing/admin/pools_routing_spec.rb
new file mode 100644
index 0000000..a49827d
--- /dev/null
+++ b/spec/routing/admin/pools_routing_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Admin::PoolsController do
+ it 'routes GET /admin/pools' do
+ expect(get('/admin/pools')).to route_to(controller: 'admin/pools', action: 'index')
+ end
+
+ it 'routes GET /admin/pools/new' do
+ expect(get('/admin/pools/new')).
+ to route_to(controller: 'admin/pools', action: 'new')
+ end
+
+ it 'routes GET /admin/pools/1' do
+ expect(get('/admin/pools/1')).
+ to route_to(controller: 'admin/pools', action: 'show', id: '1')
+ end
+
+ it 'routes GET /admin/pools/1/edit' do
+ expect(get('/admin/pools/1/edit')).
+ to route_to(controller: 'admin/pools', action: 'edit', id: '1')
+ end
+
+ it 'routes POST /admin/pools' do
+ expect(post('/admin/pools')).
+ to route_to(controller: 'admin/pools', action: 'create')
+ end
+
+ it 'routes PATCH /admin/pools/1' do
+ expect(patch('/admin/pools/1')).
+ to route_to(controller: 'admin/pools', action: 'update', id: '1')
+ end
+end