Skip to content
Merged
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
120 changes: 120 additions & 0 deletions app/controllers/api/v2/preupgrade_report_entries_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

module Api
module V2
class PreupgradeReportEntriesController < ::Api::V2::BaseController
include ApiAuthorizer
include Foreman::Controller::AutoCompleteSearch

skip_before_action :store_redirect_to_url, :reset_redirect_to_url, raise: false
before_action :find_preupgrade_report, if: -> { params[:preupgrade_report_id].present? }

api :GET, '/preupgrade_reports/:preupgrade_report_id/preupgrade_report_entries',
N_('List entries for a specific preupgrade report')
api :GET, '/preupgrade_report_entries', N_('List all preupgrade report entries')
param :preupgrade_report_id, :identifier, required: false,
desc: N_('ID of the preupgrade report')
param_group :search_and_pagination, ::Api::V2::BaseController
def index
@total = resource_scope.count
@preupgrade_report_entries = resource_scope_for_index
@subtotal = @preupgrade_report_entries.total_entries
end

api :GET, '/preupgrade_report_entries/auto_complete_search',
N_('Search autocomplete for preupgrade report entries')
param :search, String, required: false, desc: N_('Search string')
param :preupgrade_report_id, :identifier, required: false, desc: N_('ID of the preupgrade report')

api :POST, '/preupgrade_reports/:preupgrade_report_id/preupgrade_report_entries/bulk_remediate',
N_('Trigger a remediation job for selected preupgrade report entries')
param :search, String, required: false, desc: N_('Search string')
param :excluded_ids, Array, required: false, desc: N_('Array of excluded entry IDs')
def bulk_remediate
entries = filtered_remediation_entries
remediation_ids = entries.pluck(:id)

if remediation_ids.empty?
return render json: { error: _('No fixable entries found matching the selection.') },
status: :unprocessable_entity
end

composer = JobInvocationComposer.for_feature(
'leapp_remediation_plan',
target_host_ids(entries),
{ 'remediation_ids' => remediation_ids.join(',') }
)
composer.trigger!
job_invocation = composer.job_invocation

render json: { id: job_invocation.id }
rescue StandardError => e
Foreman::Logging.exception('Failed to trigger bulk remediation job', e)
render json: { error: _('An unexpected error occurred while creating the remediation job.') },
status: :internal_server_error
end

protected

def model_of_controller
PreupgradeReportEntry
end

def resource_scope(_options = {})
scope = if @preupgrade_report
@preupgrade_report.preupgrade_report_entries
else
PreupgradeReportEntry
end
scope.joins(:host).merge(Host.authorized(:view_hosts, Host))
end

def resource_scope_for_index(options = {})
order_str = params[:order].to_s
return super unless order_str.match?(/^(severity|risk_factor)\b/i)
direction = order_str.match?(/desc$/i) ? 'DESC' : 'ASC'

resource_scope(options)
.search_for(params[:search].to_s)
.order_by_severity(direction)
.paginate(paginate_options)
end

private

def filtered_remediation_entries
combined_search = [params[:search].presence, 'fix_type = command'].compact.join(' AND ')
entries = resource_scope.search_for(combined_search)
entries = entries.where.not(id: params[:excluded_ids]) if params[:excluded_ids].present?
entries
end

def target_host_ids(entries)
host_ids = entries.pluck(:host_id).uniq.compact
host_ids = [@preupgrade_report.host_id].compact if host_ids.empty? && @preupgrade_report
host_ids
end

def find_preupgrade_report
@preupgrade_report = PreupgradeReport
.joins(:host)
.merge(Host.authorized(:view_hosts, Host))
.find(params[:preupgrade_report_id])
end

def path_to_authenticate
auth_action = case action_name
when 'auto_complete_search'
'index'
when 'bulk_remediate'
'create'
else
action_name
end
path_params = params.slice(:id, :user_id)
.merge(action: auth_action, controller: 'api/v2/job_invocations')
Foreman::AccessControl.normalize_path_hash(path_params)
end
end
end
end
4 changes: 2 additions & 2 deletions app/controllers/api/v2/preupgrade_reports_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def job_invocation
# By overriding path_to_authenticate we can require REX's permission view_job_invocations
def path_to_authenticate
params['action'] = 'show' if params['action'] == 'job_invocation'
Foreman::AccessControl.normalize_path_hash params.slice(:action, :id, :user_id)
.merge({ controller: 'api/v2/job_invocations' })
path_params = params.slice(:action, :id, :user_id).merge(controller: 'api/v2/job_invocations')
Foreman::AccessControl.normalize_path_hash(path_params)
end
end
end
Expand Down
74 changes: 73 additions & 1 deletion app/models/preupgrade_report_entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,81 @@ class PreupgradeReportEntry < ApplicationRecord
serialize :flags, Array
serialize :detail, JSON

validates :preupgrade_report, :host, :hostname, :title, :actor, :audience, :severity, :leapp_run_id, presence: true
validates :preupgrade_report, :host, :hostname, :title, :actor,
:audience, :severity, :leapp_run_id, presence: true

scoped_search on: :title, complete_value: true
scoped_search on: :hostname, complete_value: true
scoped_search on: :summary

scoped_search on: :severity,
rename: :risk_factor,
complete_value: { high: 'high', medium: 'medium', low: 'low', info: 'info' }

scoped_search on: :flags,
rename: :inhibitor,
only_explicit: true,
operators: ['=', '!='],
complete_value: { yes: 'yes', no: 'no' },
ext_method: :search_yes_no_fields

scoped_search on: :detail,
rename: :has_remediation,
only_explicit: true,
operators: ['=', '!='],
complete_value: { yes: 'yes', no: 'no' },
ext_method: :search_yes_no_fields

scoped_search on: :detail,
rename: :fix_type,
only_explicit: true,
operators: ['=', '!='],
complete_value: { hint: 'hint', command: 'command' },
ext_method: :search_fix_type

# info(1) < low(2) < medium(3) < high(4)
scope :order_by_severity, lambda { |direction = 'ASC'|
dir = direction.to_s.casecmp('DESC').zero? ? 'DESC' : 'ASC'
order(Arel.sql(
"CASE preupgrade_report_entries.severity " \
"WHEN 'high' THEN 4 WHEN 'medium' THEN 3 " \
"WHEN 'low' THEN 2 WHEN 'info' THEN 1 ELSE 0 END #{dir}"
))
}

def self.remediation_details(remediation_ids, host)
where(id: remediation_ids, host: host).where.not(detail: nil).pluck(:detail)
end

def self.search_yes_no_fields(key, operator, value)
column, term = case key.to_sym
when :inhibitor then %w[flags inhibitor]
when :has_remediation then %w[detail remediations]
else raise Foreman::Exception,
N_("Unknown search key '%s' for search_yes_no_fields") % key
end

if (operator == '=' && value == 'yes') || (operator == '!=' && value == 'no')
{ conditions: "preupgrade_report_entries.#{column} LIKE '%%#{term}%%'" }
else
{ conditions: "(preupgrade_report_entries.#{column} NOT LIKE '%%#{term}%%' OR " \
"preupgrade_report_entries.#{column} IS NULL)" }
end
end

def self.search_fix_type(_key, operator, value)
unless %w[hint command].include?(value.to_s.downcase)
raise Foreman::Exception, N_("Unknown fix_type value '%s' for search_fix_type") % value
end

safe_value = sanitize_sql_like(value.to_s.downcase)
like_op = (operator == '=') ? 'LIKE' : 'NOT LIKE'

conditions = "(preupgrade_report_entries.detail #{like_op} " \
"'%%\"type\":\"#{safe_value}\"%%' OR " \
"preupgrade_report_entries.detail #{like_op} " \
"'%%\"type\": \"#{safe_value}\"%%')"
conditions = "(#{conditions} OR preupgrade_report_entries.detail IS NULL)" if operator == '!='
{ conditions: conditions }
end
end
4 changes: 4 additions & 0 deletions app/views/api/v2/preupgrade_report_entries/index.json.rabl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# frozen_string_literal: true

collection @preupgrade_report_entries
extends 'api/v2/preupgrade_report_entries/base'
7 changes: 2 additions & 5 deletions app/views/api/v2/preupgrade_reports/show.json.rabl
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
object @preupgrade_report
# frozen_string_literal: true

object @preupgrade_report
extends 'api/v2/preupgrade_reports/base'

child :preupgrade_report_entries do
extends 'api/v2/preupgrade_report_entries/base'
end
20 changes: 17 additions & 3 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
# frozen_string_literal: true

Rails.application.routes.draw do
resources :preupgrade_reports, :only => %i[index]
resources :preupgrade_reports, only: %i[index]

namespace :api, defaults: { format: 'json' } do
scope '(:apiv)', module: :v2, defaults: { apiv: 'v2' }, apiv: /v2/,
constraints: ApiConstraints.new( version: 2, default: true) do
resources :preupgrade_reports, only: %i[index show]
constraints: ApiConstraints.new(version: 2, default: true) do
resources :preupgrade_report_entries, only: %i[index] do
collection do
get :auto_complete_search
end
end

resources :preupgrade_reports, only: %i[index show] do
resources :preupgrade_report_entries, only: %i[index] do
collection do
get :auto_complete_search
post :bulk_remediate
end
end
end

get 'job_invocations/:id/preupgrade_reports', to: 'preupgrade_reports#job_invocation'
end
end
Expand Down
Loading
Loading