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.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +30 -0
- data/Rakefile +47 -0
- data/app/controllers/foreman_rescue/hosts_controller.rb +43 -0
- data/app/helpers/concerns/foreman_rescue/hosts_helper_extensions.rb +23 -0
- data/app/models/concerns/foreman_rescue/host_extensions.rb +32 -0
- data/app/models/concerns/foreman_rescue/orchestration/tftp.rb +50 -0
- data/app/models/setting/rescue.rb +43 -0
- data/app/views/foreman_rescue/hosts/rescue.html.erb +68 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20170901131321_add_rescue_mode_to_host.foreman_rescue.rb +5 -0
- data/lib/foreman_rescue.rb +4 -0
- data/lib/foreman_rescue/engine.rb +53 -0
- data/lib/foreman_rescue/version.rb +3 -0
- data/lib/tasks/foreman_rescue_tasks.rake +37 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_rescue.po +19 -0
- data/locale/foreman_rescue.pot +19 -0
- data/locale/gemspec.rb +2 -0
- data/test/factories/host.rb +7 -0
- data/test/functional/hosts_controller_test.rb +121 -0
- data/test/test_plugin_helper.rb +24 -0
- data/test/unit/host_test.rb +80 -0
- metadata +99 -0
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,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,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
|