foreman_rescue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # ForemanRescue
2
+
3
+ This plugin allows a user to boot a Foreman host into a rescue system via PXE.
4
+
5
+ ## Installation
6
+
7
+ See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
8
+ for how to install Foreman plugins.
9
+
10
+ ## Contributing
11
+
12
+ Fork and send a Pull Request. Thanks!
13
+
14
+ ## Copyright
15
+
16
+ Copyright (c) 2018 dm-drogerie markt GmbH & Co. KG, https://dm.de
17
+
18
+ This program is free software: you can redistribute it and/or modify
19
+ it under the terms of the GNU General Public License as published by
20
+ the Free Software Foundation, either version 3 of the License, or
21
+ (at your option) any later version.
22
+
23
+ This program is distributed in the hope that it will be useful,
24
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
25
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26
+ GNU General Public License for more details.
27
+
28
+ You should have received a copy of the GNU General Public License
29
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
30
+
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ForemanRescue'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+ task default: :test
37
+
38
+ begin
39
+ require 'rubocop/rake_task'
40
+ RuboCop::RakeTask.new
41
+ rescue => _
42
+ puts 'Rubocop not loaded.'
43
+ end
44
+
45
+ task :default do
46
+ Rake::Task['rubocop'].execute
47
+ end
@@ -0,0 +1,43 @@
1
+ module ForemanRescue
2
+ class HostsController < ::HostsController
3
+ before_action :find_resource, :only => [:rescue, :set_rescue, :cancel_rescue]
4
+ define_action_permission ['rescue', 'set_rescue', 'cancel_rescue'], :rescue
5
+
6
+ def rescue; end
7
+
8
+ def set_rescue
9
+ forward_url_options
10
+ if @host.set_rescue
11
+ if params[:host] && params[:host][:rescue_mode] == '1'
12
+ begin
13
+ message = if @host.power.reset
14
+ _('Enabled %s for reboot into rescue system.')
15
+ else
16
+ _('Enabled %s for boot into rescue system on next boot, but failed to power cycle the host.')
17
+ end
18
+ process_success :success_msg => message % @host, :success_redirect => :back
19
+ rescue => error
20
+ message = _('Failed to reboot %s.') % @host
21
+ warning(message)
22
+ Foreman::Logging.exception(message, error)
23
+ process_success :success_msg => _('Enabled %s for rescue system on next boot.') % @host, :success_redirect => :back
24
+ end
25
+ else
26
+ process_success :success_msg => _('Enabled %s for rescue system on next boot.') % @host, :success_redirect => :back
27
+ end
28
+ else
29
+ process_error :redirect => :back, :error_msg => _('Failed to enable %{host} for rescue system: %{errors}') % { :host => @host, :errors => @host.errors.full_messages.to_sentence }
30
+ end
31
+ end
32
+
33
+ def cancel_rescue
34
+ if @host.cancel_rescue
35
+ process_success :success_msg => _('Canceled booting into rescue system for %s.') % @host.name, :success_redirect => :back
36
+ else
37
+ process_error :redirect => :back,
38
+ :error_msg => _('Failed to cancel booting into rescue system for %{hostname} with the following errors: %{errors}') %
39
+ { :hostname => @host.name, :errors => @host.errors.full_messages.to_sentence }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ module ForemanRescue
2
+ module HostsHelperExtensions
3
+ def host_title_actions(host)
4
+ title_actions(
5
+ button_group(
6
+ if host.rescue_mode?
7
+ link_to_if_authorized(_('Cancel rescue'), hash_for_cancel_rescue_host_path(:id => host).merge(:auth_object => host, :permission => 'rescue_hosts'),
8
+ :disabled => host.can_be_rescued?,
9
+ :title => _('Cancel rescue system for this host.'),
10
+ :class => 'btn btn-default',
11
+ :method => :put)
12
+ else
13
+ link_to_if_authorized(_('Rescue'), hash_for_rescue_host_path(:id => host).merge(:auth_object => host, :permission => 'rescue_hosts'),
14
+ :disabled => !host.can_be_rescued?,
15
+ :title => _('Activate rescue mode for this host.'),
16
+ :class => 'btn btn-default')
17
+ end
18
+ )
19
+ )
20
+ super
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,32 @@
1
+ module ForemanRescue
2
+ module HostExtensions
3
+ def self.prepended(base)
4
+ base.class_eval do
5
+ validate :build_and_rescue_mode
6
+ end
7
+ end
8
+
9
+ def can_be_rescued?
10
+ managed? && SETTINGS[:unattended] && pxe_build? && !build? && !rescue_mode?
11
+ end
12
+
13
+ def can_be_built?
14
+ super && !rescue_mode?
15
+ end
16
+
17
+ def set_rescue
18
+ update(rescue_mode: true)
19
+ end
20
+
21
+ def cancel_rescue
22
+ update(rescue_mode: false)
23
+ end
24
+
25
+ private
26
+
27
+ def build_and_rescue_mode
28
+ errors.add(:base, 'can either be in build mode or rescue mode, not both.') if build? && rescue_mode?
29
+ true
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,50 @@
1
+ module ForemanRescue
2
+ module Orchestration
3
+ module TFTP
4
+ def self.prepended(base)
5
+ base.class_eval do
6
+ after_validation :queue_tftp_rescue
7
+ delegate :rescue_mode, :rescue_mode?, :to => :host
8
+ end
9
+ end
10
+
11
+ def queue_tftp_rescue
12
+ return unless tftp? || tftp6?
13
+ return if new_record?
14
+ queue_tftp_update_rescue unless new_record?
15
+ end
16
+
17
+ # Overwritten because we do not want to
18
+ # queue the tasks multiple times
19
+ def queue_tftp_create
20
+ super unless tftp_queued?
21
+ end
22
+
23
+ def queue_tftp_update_rescue
24
+ queue_tftp_create if old.host.rescue_mode? != host.rescue_mode?
25
+ end
26
+
27
+ private
28
+
29
+ # We have to overwrite default_pxe_render to hook
30
+ # in the rescue tftp template rendering
31
+ def default_pxe_render(kind)
32
+ return rescue_mode_pxe_render(kind) if rescue_mode?
33
+ super
34
+ end
35
+
36
+ def rescue_mode_pxe_render(kind)
37
+ template_name = Setting["rescue_#{kind.downcase}_tftp_template"]
38
+ template = ::ProvisioningTemplate.find_by_name(template_name)
39
+ return unless template.present?
40
+ unattended_render template
41
+ rescue => e
42
+ failure _("Unable to render %{kind} rescue template '%{name}' for TFTP: %{e}") % { :kind => kind, :name => template.try(:name), :e => e }, e
43
+ end
44
+
45
+ def tftp_queued?
46
+ queue.all.select { |task| task.action[1] == :setTFTP }.present?
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,43 @@
1
+ class Setting
2
+ class Rescue < ::Setting
3
+ def self.default_settings
4
+ [
5
+ set('rescue_pxelinux_tftp_template',
6
+ N_('PXELinux template used when booting rescue system'),
7
+ 'create', N_('PXELinux rescue template'), nil,
8
+ { :collection => proc { Setting::Rescue.templates('PXELinux') } }),
9
+ set('rescue_pxegrub_tftp_template',
10
+ N_('PXEGrub template used when booting rescue system'),
11
+ 'create', N_('PXEGrub rescue template'), nil,
12
+ { :collection => proc { Setting::Rescue.templates('PXEGrub') } }),
13
+ set('rescue_pxegrub2_tftp_template',
14
+ N_('PXEGrub2 template used when booting rescue system'),
15
+ 'create', N_('PXEGrub2 rescue template'), nil,
16
+ { :collection => proc { Setting::Rescue.templates('PXEGrub 2') } })
17
+ ]
18
+ end
19
+
20
+ def self.load_defaults
21
+ # Check the table exists
22
+ return unless super
23
+
24
+ self.transaction do
25
+ default_settings.each { |s| self.create! s.update(:category => 'Setting::Rescue') }
26
+ end
27
+
28
+ true
29
+ end
30
+
31
+ def self.templates(kind)
32
+ template_kind = TemplateKind.find_by_name(kind)
33
+ templates = ProvisioningTemplate.where(:template_kind => template_kind)
34
+ templates.each_with_object({}) do |template, hsh|
35
+ hsh[template.name] = template.name
36
+ end
37
+ end
38
+
39
+ def self.humanized_category
40
+ N_('Rescue System')
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,68 @@
1
+ <% title @host.to_label, icon(@host.operatingsystem) + @host.to_label %>
2
+ <div class="blank-slate-pf">
3
+ <div class="blank-slate-pf-icon">
4
+ <%= icon_text('medkit', '', :kind => 'fa', :class => @host.rescue_mode? ? 'text-success' : '') %>
5
+ </div>
6
+ <h1><%= _('Rescue Mode') %></h1>
7
+ <p>
8
+ <% if @host.rescue_mode? %>
9
+ <%= _("This host has rescue mode enabled.") %></br>
10
+ <% elsif !@host.can_be_rescued? %>
11
+ <%= _("This host can not be booted into rescue mode.") %></br>
12
+ <% else %>
13
+ <%= _("This host does not have rescue mode enabled.") %></br>
14
+ <% end %>
15
+ </p>
16
+ <div class="blank-slate-pf-main-action">
17
+ <% if @host.rescue_mode? %>
18
+ <%= link_to_if_authorized(_("Back to host"), hash_for_host_path(:id => @host), :title => _("Back to host"), :class => 'btn btn-default btn-lg') %>
19
+ <%= link_to_if_authorized(
20
+ _('Cancel rescue mode'),
21
+ hash_for_cancel_rescue_host_path(:id => @host).merge(:auth_object => @host, :permission => 'rescue_hosts'),
22
+ :method => :put,
23
+ :class => 'btn btn-primary btn-lg')
24
+ %>
25
+ <% elsif !@host.can_be_rescued? %>
26
+ <div class="alert alert-info" style="text-align: left; font-size: 0.8em;">
27
+ <%= alert_header(_('The following reasons prevent a boot into rescue mode:'), 'alert-info') %>
28
+ <ul>
29
+ <% unless @host.managed? %>
30
+ <li><%= _('%s is not managed.') % @host %></li>
31
+ <% end %>
32
+ <% unless SETTINGS[:unattended] %>
33
+ <li><%= _('Unattended mode is not activated.') %></li>
34
+ <% end %>
35
+ <% unless @host.pxe_build? %>
36
+ <li><%= _('%s does not support PXE booting.') % @host %></li>
37
+ <% end %>
38
+ <% if @host.build? %>
39
+ <li><%= _('%s is in build mode.') % @host %></li>
40
+ <% end %>
41
+ </ul>
42
+ </div>
43
+ <%= link_to_if_authorized(_("Back to host"), hash_for_host_path(:id => @host), :title => _("Back to host"), :class => 'btn btn-default btn-lg') %>
44
+ <% else %>
45
+ <div class="panel panel-default">
46
+ <div class="panel-heading">
47
+ <h3 class="panel-title"><%= _('Activate rescue mode') %></h3>
48
+ </div>
49
+ <div class="panel-body">
50
+ <%= form_for :host,
51
+ :method => :put,
52
+ :html => {:class => 'form-horizontal'},
53
+ :url => hash_for_set_rescue_host_path(:id => @host).merge(:auth_object => @host, :permission => 'rescue_hosts') do |f|
54
+ %>
55
+ <div class="alert alert-warning" style="text-align: left; font-size: 0.8em;">
56
+ <span class="pficon pficon-warning-triangle-o"></span>
57
+ <%= _("Activating rescue mode can disrupt running services on %s.") % @host %>
58
+ </div>
59
+
60
+ <%= checkbox_f(f, :rescue_mode, :help_text => _('Reboot %s') % @host, :size => '') if @host.supports_power_and_running? %>
61
+ <%= link_to_if_authorized(_("Back to host"), hash_for_host_path(:id => @host), :title => _("Back to host"), :class => 'btn btn-default btn-lg') %>
62
+ <%= f.submit _("Activate rescue mode"), :class => "btn btn-danger btn-lg submit" %>
63
+ <% end %>
64
+ <% end %>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,11 @@
1
+ Rails.application.routes.draw do
2
+ constraints(:id => /[^\/]+/) do
3
+ resources :hosts, controller: 'foreman_rescue/hosts', :only => [] do
4
+ member do
5
+ get 'rescue'
6
+ put 'set_rescue'
7
+ put 'cancel_rescue'
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ class AddRescueModeToHost < ActiveRecord::Migration
2
+ def change
3
+ add_column :hosts, :rescue_mode, :boolean, default: false, index: true
4
+ end
5
+ end
@@ -0,0 +1,4 @@
1
+ require 'foreman_rescue/engine'
2
+
3
+ module ForemanRescue
4
+ end
@@ -0,0 +1,53 @@
1
+ module ForemanRescue
2
+ class Engine < ::Rails::Engine
3
+ engine_name 'foreman_rescue'
4
+
5
+ config.autoload_paths += Dir["#{config.root}/app/helpers/concerns"]
6
+ config.autoload_paths += Dir["#{config.root}/app/models/concerns"]
7
+
8
+ # Add any db migrations
9
+ initializer 'foreman_rescue.load_app_instance_data' do |app|
10
+ ForemanRescue::Engine.paths['db/migrate'].existent.each do |path|
11
+ app.config.paths['db/migrate'] << path
12
+ end
13
+ end
14
+
15
+ initializer 'foreman_monitoring.load_default_settings',
16
+ :before => :load_config_initializers do |_app|
17
+ if begin
18
+ Setting.table_exists?
19
+ rescue
20
+ false
21
+ end
22
+ require_dependency File.expand_path('../../../app/models/setting/rescue.rb', __FILE__)
23
+ end
24
+ end
25
+
26
+ initializer 'foreman_rescue.register_plugin', :before => :finisher_hook do |_app|
27
+ Foreman::Plugin.register :foreman_rescue do
28
+ requires_foreman '>= 1.15'
29
+
30
+ # Add permissions
31
+ security_block :foreman_rescue do
32
+ permission :rescue_hosts, :'foreman_rescue/hosts' => [:rescue, :set_rescue, :cancel_rescue]
33
+ end
34
+ end
35
+ end
36
+
37
+ config.to_prepare do
38
+ begin
39
+ Host::Managed.send(:prepend, ForemanRescue::HostExtensions)
40
+ HostsHelper.send(:prepend, ForemanRescue::HostsHelperExtensions)
41
+ Nic::Managed.send(:prepend, ForemanRescue::Orchestration::TFTP)
42
+ rescue => e
43
+ Rails.logger.warn "ForemanRescue: skipping engine hook (#{e})"
44
+ end
45
+ end
46
+
47
+ initializer 'foreman_rescue.register_gettext', after: :load_config_initializers do |_app|
48
+ locale_dir = File.join(File.expand_path('../../..', __FILE__), 'locale')
49
+ locale_domain = 'foreman_rescue'
50
+ Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,3 @@
1
+ module ForemanRescue
2
+ VERSION = '0.1.0'.freeze
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'rake/testtask'
2
+
3
+ # Tests
4
+ namespace :test do
5
+ desc 'Test ForemanRescue'
6
+ Rake::TestTask.new(:foreman_rescue) do |t|
7
+ test_dir = File.join(File.dirname(__FILE__), '../..', 'test')
8
+ t.libs << ['test', test_dir]
9
+ t.pattern = "#{test_dir}/**/*_test.rb"
10
+ t.verbose = true
11
+ t.warning = false
12
+ end
13
+ end
14
+
15
+ namespace :foreman_rescue do
16
+ task :rubocop do
17
+ begin
18
+ require 'rubocop/rake_task'
19
+ RuboCop::RakeTask.new(:rubocop_foreman_rescue) do |task|
20
+ task.patterns = ["#{ForemanRescue::Engine.root}/app/**/*.rb",
21
+ "#{ForemanRescue::Engine.root}/lib/**/*.rb",
22
+ "#{ForemanRescue::Engine.root}/test/**/*.rb"]
23
+ end
24
+ rescue
25
+ puts 'Rubocop not loaded.'
26
+ end
27
+
28
+ Rake::Task['rubocop_foreman_rescue'].invoke
29
+ end
30
+ end
31
+
32
+ Rake::Task[:test].enhance ['test:foreman_rescue']
33
+
34
+ load 'tasks/jenkins.rake'
35
+ if Rake::Task.task_defined?(:'jenkins:unit')
36
+ Rake::Task['jenkins:unit'].enhance ['test:foreman_rescue', 'foreman_rescue:rubocop']
37
+ end