foreman_rescue 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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