Page MenuHomeGRNET

No OneTemporary

File Metadata

Created
Sun, Nov 24, 3:58 PM
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])
#
# <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
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 %>
+
+ <div class="form-group">
+ <div class="col-xs-offset-8 col-xs-3">
+ <%= f.submit class: 'btn btn-success' %>
+ </div>
+ </div>
+<% end %>
+
+<%= link_to 'Cancel', admin_pools_path, class: 'btn btn-danger', role: 'button' %>
diff --git a/app/views/admin/pools/_pool.html.erb b/app/views/admin/pools/_pool.html.erb
new file mode 100644
index 0000000..6300b7d
--- /dev/null
+++ b/app/views/admin/pools/_pool.html.erb
@@ -0,0 +1,14 @@
+<tr>
+ <td><%= link_to "##{pool.id}", admin_pool_path(pool) %></td>
+ <td><%= link_to pool.name, admin_pool_path(pool) %></td>
+ <td><%= pool.max_vols %></td>
+ <td><%= yes_no(pool.use_once) %></td>
+ <td><%= pool.vol_retention_human %></td>
+ <td><%= pool.max_vol_jobs %></td>
+ <td><%= pool.max_vol_files %></td>
+ <td><%= number_to_human_size pool.max_vol_bytes %></td>
+ <td><%= yes_no(pool.auto_prune) %></td>
+ <td><%= pool.label_format %></td>
+ <td><%= yes_no(pool.recycle) %></td>
+ <td><%= pool.pool_type %></td>
+</tr>
diff --git a/app/views/admin/pools/index.html.erb b/app/views/admin/pools/index.html.erb
new file mode 100644
index 0000000..3ee79de
--- /dev/null
+++ b/app/views/admin/pools/index.html.erb
@@ -0,0 +1,37 @@
+<div class="row right">
+ <%= link_to new_admin_pool_path, class: "btn btn-default", role: "button" do %>
+ <label class="glyphicon glyphicon-plus text-primary"></label>
+ New Pool
+ <% end %>
+</div>
+
+<h1>Pools</h1>
+
+<div class="row">
+ <div class="col-xs-12">
+ <div class="table-responsive">
+ <table class="table table-striped table-bordered table-condensed">
+ <thead>
+ <tr>
+ <th>id</th>
+ <th>Name</th>
+ <th>Max Volumes</th>
+ <th>Use Once</th>
+ <th>Volume Retention</th>
+ <th>Max Volume Jobs</th>
+ <th>Max Volume Files</th>
+ <th>Max Volume Bytes</th>
+ <th>Auto Prune</th>
+ <th>Label Format</th>
+ <th>Recycle</th>
+ <th>Pool Type</th>
+ </tr>
+ </thead>
+
+ <tbody>
+ <%= render partial: 'pool', collection: @pools %>
+ </tbody>
+ </table>
+ </div>
+ </div>
+</div>
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 @@
+<div class="row">
+ <div class="col-xs-4">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ <h3>New Pool</h3>
+
+ <%= render partial: 'form' %>
+ </div>
+ </div>
+ </div>
+</div>
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 @@
<!-- Fixed navbar -->
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="logo">
<%= link_to root_path do %>
<p><b>GRNET NOC</b></p>
<p>Archiving</p>
<% end %>
</li>
<% if current_user %>
<%= content_tag(:li, active_class(clients_path, true)) do %>
<%= link_to 'Clients', clients_path %>
<% end %>
<li><%= link_to current_user.username, '#' %></li>
<% end %>
</ul>
<% if current_user %>
<ul class="nav navbar-nav navbar-right">
<li>
<%= link_to logout_path do %>
<label class="glyphicon glyphicon-log-out"></label>
Logout
<% end %>
</li>
</ul>
<% end %>
<% if current_user.try(:admin?) %>
<ul class="nav navbar-nav navbar-right">
<%= content_tag(:li, active_class(admin_path)) do %>
<%= link_to 'Admin', admin_path %>
<% end %>
<%= content_tag(:li, active_class(admin_clients_path, true)) do %>
<%= link_to 'Clients', admin_clients_path %>
<% end %>
<%= content_tag(:li, active_class(unverified_admin_hosts_path, true)) do %>
<%= link_to 'Hosts', unverified_admin_hosts_path %>
<% end %>
<%= content_tag(:li, { class: "dropdown #{active_class(admin_users_path)[:class]}" }) do %>
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
Users <span class="caret"></span>
</a>
<ul class="dropdown-menu">
<li>
<%= link_to 'All Users', admin_users_path %>
</li>
<li class="divider"></li>
<li>
<%= link_to 'ViMa Users', admin_users_path(type: :vima) %>
</li>
<li>
<%= link_to 'Institutional Users', admin_users_path(type: :institutional) %>
</li>
<li>
<%= link_to 'Admins', admin_users_path(type: :admin) %>
</li>
<li>
</ul>
<% end %>
+ <%= content_tag(:li, active_class(admin_pools_path)) do %>
+ <%= link_to 'Pools', admin_pools_path %>
+ <% end %>
<%= content_tag(:li, active_class(admin_settings_path)) do %>
<%= link_to 'Settings', admin_settings_path %>
<% end %>
</ul>
<% end %>
</div><!--/.nav-collapse -->
</div>
</nav>
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

Event Timeline