Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes #10756, #13500 - developer api and PXE-less reprovisioning #144

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions app/controllers/api/v2/job_invocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,8 @@ def show
def create
composer = JobInvocationComposer.from_api_params(job_invocation_params)
composer.save!
@job_invocation = composer.job_invocation
@job_invocation.generate_description! if @job_invocation.description.blank?
composer.triggering.trigger(::Actions::RemoteExecution::RunHostsJob, @job_invocation)
process_response @job_invocation
composer.trigger
process_response composer.job_invocation
end

api :GET, '/job_invocations/:id/hosts/:host_id', N_('Get output for a host')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module ForemanRemoteExecution
module HostsControllerExtensions
extend ActiveSupport::Concern
include Foreman::Renderer

included do
alias_method_chain(:action_permission, :remote_execution)
end

def reprovision
find_resource
script_template = @host.provisioning_template(:kind => 'script')
if script_template.nil?
process_error :redirect => :back, :error_msg => _("No script provisioning template available")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Single quote string literals (and in a bunch of other places) 😄 rubocop can fix all of these.

return
end
@host.setBuild
script = unattended_render(script_template.template, @template_name)
composer = JobInvocationComposer.for_feature(:reprovision, @host, :script => script)
composer.save!
composer.trigger
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this 3-step dance in a lot of places, we don't have to change it now, but any reason not to just save and trigger in one step for this and other controllers? Something like

JobInvocationComposer.trigger_for_feature!(:reprovision, @host, :script => script)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so there would be both JobInvocationComposer.trigger_for_feature(:reprovision, @host, :script => script) and JobInvocationComposer.trigger_for_feature!(:reprovision, @host, :script => script) (the difference is in save and save!). Maybe JobInvocationComposer.for_feature(:reprovision, @host, :script => script).trigger and JobInvocationComposer.for_feature(:reprovision, @host, :script => script).trigger!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, that would work. It's really not a big deal, I just noticed we keep making minor changes to how this gets called and figured maybe it could be DRYer.

process_success :success_msg => _("Reprovision job started. The host should reboot soon."), :success_redirect => :back
end

private

def action_permission_with_remote_execution
case params[:action]
when 'reprovision'
:edit_host
else
action_permission_without_remote_execution
end
end
end
end
8 changes: 3 additions & 5 deletions app/controllers/job_invocations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def rerun

if params[:failed_only]
host_ids = job_invocation.failed_host_ids
@composer.search_query = @composer.targeting.build_query_from_hosts(host_ids)
@composer.search_query = Targeting.build_query_from_hosts(host_ids)
end

render :action => 'new'
Expand All @@ -39,10 +39,8 @@ def rerun
def create
@composer = JobInvocationComposer.from_ui_params(params)
if @composer.save
job_invocation = @composer.job_invocation
job_invocation.generate_description! if job_invocation.description.blank?
@composer.triggering.trigger(::Actions::RemoteExecution::RunHostsJob, job_invocation)
redirect_to job_invocation_path(job_invocation)
@composer.trigger
redirect_to job_invocation_path(@composer.job_invocation)
else
@composer.job_invocation.description_format = nil if params[:job_invocation].key?(:description_override)
render :action => 'new'
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/remote_execution_features_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
class RemoteExecutionFeaturesController < ::ApplicationController
before_filter :find_resource, :only => [:show, :update]

def index
@remote_execution_features = resource_base.all
end

def show
end

def update
if @remote_execution_feature.update_attributes(params[:remote_execution_feature])
process_success :object => @remote_execution_feature
else
process_error :object => @remote_execution_feature
end
end

end
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ def multiple_actions_with_remote_execution
multiple_actions_without_remote_execution + [[_('Run Job'), new_job_invocation_path, false]]
end

def host_title_actions_with_run_button(*args)
title_actions(button_group(link_to(_('Run Job'), new_job_invocation_path(:host_ids => [args.first.id]), :id => :run_button)))
host_title_actions_without_run_button(*args)
def host_title_actions_with_run_button(host)
links = [link_to(_('Run Job'), new_job_invocation_path(:host_ids => [host.id]), :id => :run_button)]
if RemoteExecutionFeature.feature(:reprovision).template && @host.provisioning_template(:kind => 'script')
links << link_to(_('Reprovision'), reprovision_host_path(host.id), { :method => :post, :disabled => @host.build })
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we somehow add this to the build button as a dropdown? I would guess maybe not easy to extend from plugin.

I only wonder because the host actions can get quite big if you install a lot of plugins, and this is really related to the build.

end
title_actions(button_group(*links))
host_title_actions_without_run_button(host)
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def remote_execution_proxies(provider)
proxy_scope = ::SmartProxy
end

proxies[:global] = proxy_scope.authorized.with_features(provider)
proxies[:global] = proxy_scope.with_features(provider)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to do this in order to ssh keys snippet to work with global proxy, I will probably open a separate PR and issue for this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

end

proxies
Expand Down
1 change: 1 addition & 0 deletions app/models/input_template_renderer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ class InputTemplateRenderer
class UndefinedInput < ::Foreman::Exception
end

include Rails.application.routes.url_helpers
include UnattendedHelper

attr_accessor :template, :host, :invocation, :input_values, :error_message
Expand Down
71 changes: 70 additions & 1 deletion app/models/job_invocation_composer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,66 @@ def triggering_params
end
end

class ParamsForFeature
attr_reader :feature_label, :feature, :provided_inputs

def initialize(feature_label, hosts, provided_inputs = {})
@feature = RemoteExecutionFeature.feature(feature_label)
@provided_inputs = provided_inputs
if hosts.is_a? Bookmark
@host_bookmark = hosts
elsif hosts.is_a? Host::Base
@host_objects = [hosts]
elsif hosts.is_a? String
@host_scoped_search = hosts
else
@host_objects = hosts
end
end

def params
{ :job_category => template.job_category,
:targeting => targeting_params,
:triggering => {},
:template_invocations => template_invocations_params,
:description_format => template.generate_description_format }.with_indifferent_access
end

private

def targeting_params
ret = {}
ret['targeting_type'] = Targeting::STATIC_TYPE
ret['search_query'] = @host_scoped_search if @host_scoped_search
ret['search_query'] = Targeting.build_query_from_hosts(@host_objects) if @host_objects
ret['bookmark_id'] = @host_bookmark.id if @host_bookmark
ret['user_id'] = User.current.id
ret
end

def template_invocations_params
[ { 'template_id' => template.id,
'input_values' => input_values_params } ]
end

def input_values_params
@provided_inputs.map do |key, value|
input = template.template_inputs.find_by_name!(key)
{ 'template_input_id' => input.id, 'value' => value }
end
end

def template
template = JobTemplate.authorized(:view_job_templates).find_by_id(feature.template_id)
unless template
raise Foreman::Exception.new(N_('The template %{template_name} mapped to feature %{feature_name} is not accessible by the user'),
:template_name => mapping.template.name,
:feature_name => feature.name)
end
template
end
end

attr_accessor :params, :job_invocation, :host_ids, :search_query
delegate :job_category, :pattern_template_invocations, :template_invocations, :targeting, :triggering, :to => :job_invocation

Expand All @@ -190,6 +250,10 @@ def self.from_api_params(api_params)
self.new(ApiParams.new(api_params).params)
end

def self.for_feature(feature_label, hosts, provided_inputs = {})
self.new(ParamsForFeature.new(feature_label, hosts, provided_inputs).params)
end

def compose
job_invocation.job_category = validate_job_category(params[:job_category])
job_invocation.job_category ||= available_job_categories.first if @set_defaults
Expand All @@ -201,6 +265,11 @@ def compose
self
end

def trigger
job_invocation.generate_description! if job_invocation.description.blank?
triggering.trigger(::Actions::RemoteExecution::RunHostsJob, job_invocation)
end

def valid?
targeting.valid? & job_invocation.valid? & !pattern_template_invocations.map(&:valid?).include?(false)
end
Expand Down Expand Up @@ -262,7 +331,7 @@ def displayed_search_query
if @search_query.present?
@search_query
elsif host_ids.present?
targeting.build_query_from_hosts(host_ids)
Targeting.build_query_from_hosts(host_ids)
elsif targeting.bookmark_id
if (bookmark = available_bookmarks.find_by(:id => targeting.bookmark_id))
bookmark.query
Expand Down
9 changes: 8 additions & 1 deletion app/models/job_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class NonUniqueInputsError < Foreman::Exception
has_many :pattern_template_invocations, :conditions => 'host_id IS NULL', :foreign_key => 'template_id', :class_name => 'TemplateInvocation'
end

has_many :remote_execution_features, :dependent => :nullify, :foreign_key => 'template_id'

# these can't be shared in parent class, scoped search can't handle STI properly
# tested with scoped_search 3.2.0
include Taxonomix
Expand Down Expand Up @@ -70,13 +72,18 @@ def self.import(template, options = {})

inputs = metadata.delete('template_inputs')

template = self.create(metadata.merge(:template => template.gsub(/<%\#.+?.-?%>\n?/m, '')).merge(options))
template = self.create(metadata.merge(:template => template.gsub(/<%\#.+?.-?%>\n?/m, '')).except('feature').merge(options))
template.assign_taxonomies

inputs.each do |input|
template.template_inputs << TemplateInput.create(input)
end

if metadata['feature'] && feature = RemoteExecutionFeature.feature(metadata['feature'])
feature.template_id ||= template.id
feature.save!
end

template
end

Expand Down
36 changes: 36 additions & 0 deletions app/models/remote_execution_feature.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
class RemoteExecutionFeature < ActiveRecord::Base
attr_accessible :label, :name, :provided_input_names, :description, :template_id

validate :label, :name, :presence => true, :unique => true

belongs_to :template

extend FriendlyId
friendly_id :label

def provided_input_names
self.provided_inputs.to_s.split(',').map(&:chomp)
end

def provided_input_names=(values)
self.provided_inputs = Array(values).join(',')
end

class << self
def feature(label)
self.find_by_label(label) || raise(::Foreman::Exception.new(N_("Unknown remote execution feature %s"), label))
end

def register(label, name, options = {})
return false unless RemoteExecutionFeature.table_exists?
options.assert_valid_keys(:provided_inputs, :description)
feature = self.find_by_label(label)
if feature.nil?
feature = self.create!(:label => label, :name => name, :provided_input_names => options[:provided_inputs], :description => options[:description])
else
feature.update_attributes!(:name => name, :provided_input_names => options[:provided_inputs], :description => options[:description])
end
return feature
end
end
end
4 changes: 2 additions & 2 deletions app/models/targeting.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def static?
targeting_type == STATIC_TYPE
end

def build_query_from_hosts(ids)
def self.build_query_from_hosts(ids)
hosts = Host.where(:id => ids).all.group_by(&:id)
ids.map { |id| "name = #{hosts[id].first.name}" }.join(' or ')
hosts.map { |id, h| "name = #{h.first.name}" }.join(' or ')
end

def resolved?
Expand Down
18 changes: 18 additions & 0 deletions app/views/remote_execution_features/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<%= form_for @remote_execution_feature do |f| %>
<div class="row">
<%= field(f, :name) { @remote_execution_feature.name } %>
</div>
<div class="row">
<%= field(f, :label) { @remote_execution_feature.label } %>
</div>
<div class="row">
<%= field(f, :description) { @remote_execution_feature.description } %>
</div>
<div class="row">
<%= field(f, :provided_inputs) { @remote_execution_feature.provided_inputs } %>
</div>
<div class="row">
<%= selectable_f f, :template_id, JobTemplate.all.map { |t| [ t.name, t.id ] }, { :include_blank => true }, :class => 'input_type_selector' %>
</div>
<%= submit_or_cancel f %>
<% end %>
21 changes: 21 additions & 0 deletions app/views/remote_execution_features/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<% title _('Remote Execution Features') %>

<table class="table table-bordered table-striped table-condensed">
<thead>
<tr>
<th><%= sort :label, :as => _('Label') %></th>
<th><%= sort :name, :as => _('Name') %></th>
<th><%= sort :description, :as => _('Description') %></th>
</tr>
</thead>

<tbody>
<% @remote_execution_features.each do |feature| %>
<tr>
<td><%= link_to_if_authorized feature.label, hash_for_remote_execution_feature_path(feature).merge(:auth_object => feature, :permission => :view_remote_execution_feature) %></td>
<td><%= feature.name %></td>
<td><%= feature.description %></td>
</tr>
<% end %>
</tbody>
</table>
3 changes: 3 additions & 0 deletions app/views/remote_execution_features/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<% title _("Edit Remote Execution Feature") %>

<%= render :partial => 'form' %>
22 changes: 22 additions & 0 deletions app/views/templates/power_action.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<%#
kind: job_template
name: Power Action - SSH Default
job_category: Power
description_format: '%{action} host'
provider_type: SSH
template_inputs:
- name: action
description: Action to perform on the service
input_type: user
options: "restart\nshutdown"
required: true
%>

echo <%= input('action') %> host && sleep 3
<%= case input('action')
when 'restart'
'reboot'
else
'shutdown -h now'
end %>

16 changes: 16 additions & 0 deletions app/views/templates/reprovision.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<%#
kind: job_template
name: Reprovision - SSH Default
job_category: Power
description_format: 'Reprovision host'
feature: reprovision
provider_type: SSH
template_inputs:
- name: script
description: script to configure the bootloader to provision on next boot
input_type: user
required: true
%>

<%= input('script') %>
<%= render_template("Power Action - SSH Default", :action => "restart") %>
Loading