foreman_rh_cloud 5.0.32 → 5.0.35

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/rh_cloud/cloud_request_controller.rb +83 -0
  3. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +7 -0
  4. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +13 -2
  5. data/app/controllers/insights_cloud/hits_controller.rb +8 -1
  6. data/app/models/setting/rh_cloud.rb +2 -1
  7. data/app/services/foreman_rh_cloud/cloud_connector.rb +1 -1
  8. data/app/services/foreman_rh_cloud/cloud_presence.rb +124 -0
  9. data/app/services/foreman_rh_cloud/cloud_request.rb +8 -1
  10. data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +16 -5
  11. data/app/services/foreman_rh_cloud/hit_remediations_retriever.rb +67 -0
  12. data/app/services/foreman_rh_cloud/remediations_retriever.rb +16 -45
  13. data/app/services/foreman_rh_cloud/template_renderer_helper.rb +13 -1
  14. data/app/services/foreman_rh_cloud/url_remediations_retriever.rb +37 -0
  15. data/app/views/job_templates/cloud_connector.erb +30 -0
  16. data/app/views/job_templates/rh_cloud_download_playbook.erb +26 -0
  17. data/config/routes.rb +2 -0
  18. data/lib/foreman_rh_cloud/engine.rb +33 -12
  19. data/lib/foreman_rh_cloud/version.rb +1 -1
  20. data/lib/foreman_rh_cloud.rb +4 -0
  21. data/lib/insights_cloud/async/connector_playbook_execution_reporter_task.rb +193 -0
  22. data/lib/insights_cloud/async/insights_scheduled_sync.rb +4 -0
  23. data/lib/insights_cloud/generators/playbook_progress_generator.rb +49 -0
  24. data/lib/tasks/insights.rake +13 -0
  25. data/package.json +1 -1
  26. data/test/controllers/insights_cloud/api/cloud_request_controller_test.rb +78 -0
  27. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +16 -0
  28. data/test/jobs/connector_playbook_execution_reporter_task_test.rb +207 -0
  29. data/test/unit/playbook_progress_generator_test.rb +75 -0
  30. data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +13 -8
  31. data/test/unit/services/foreman_rh_cloud/{remediations_retriever_test.rb → hit_remediations_retriever_test.rb} +3 -3
  32. data/test/unit/services/foreman_rh_cloud/url_remediations_retriever_test.rb +27 -0
  33. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableActions.js +6 -11
  34. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableHelpers.js +22 -0
  35. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediationActions.js +6 -1
  36. data/webpack/InsightsHostDetailsTab/InsightsTotalRiskChart.js +3 -2
  37. metadata +20 -4
@@ -6,15 +6,18 @@ module ForemanRhCloud
6
6
  engine_name 'foreman_rh_cloud'
7
7
 
8
8
  def self.register_scheduled_task(task_class, cronline)
9
- return if ForemanTasks::RecurringLogic.joins(:tasks)
10
- .merge(ForemanTasks::Task.where(label: task_class.name))
11
- .exists?
12
-
13
- User.as_anonymous_admin do
14
- recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline(cronline)
15
- recurring_logic.save!
16
- recurring_logic.start(task_class)
9
+ ForemanTasks::RecurringLogic.transaction(isolation: :serializable) do
10
+ return if ForemanTasks::RecurringLogic.joins(:tasks)
11
+ .merge(ForemanTasks::Task.where(label: task_class.name))
12
+ .exists?
13
+
14
+ User.as_anonymous_admin do
15
+ recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline(cronline)
16
+ recurring_logic.save!
17
+ recurring_logic.start(task_class)
18
+ end
17
19
  end
20
+ rescue ActiveRecord::TransactionIsolationError
18
21
  end
19
22
 
20
23
  initializer 'foreman_rh_cloud.load_default_settings', :before => :load_config_initializers do
@@ -76,9 +79,13 @@ module ForemanRhCloud
76
79
  },
77
80
  :resource_type => ::InsightsHit.name
78
81
  )
82
+ permission(
83
+ :dispatch_cloud_requests,
84
+ 'api/v2/rh_cloud/cloud_request': [:update]
85
+ )
79
86
  end
80
87
 
81
- plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits]
88
+ plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests]
82
89
 
83
90
  role 'ForemanRhCloud', plugin_permissions, 'Role granting permissions to view the hosts inventory,
84
91
  generate a report, upload it to the cloud and download it locally'
@@ -116,7 +123,7 @@ module ForemanRhCloud
116
123
  end
117
124
 
118
125
  extend_template_helpers ForemanRhCloud::TemplateRendererHelper
119
- allowed_template_helpers :remediations_playbook
126
+ allowed_template_helpers :remediations_playbook, :download_rh_playbook
120
127
  end
121
128
 
122
129
  ::Katello::UINotifications::Subscriptions::ManifestImportSuccess.include ForemanInventoryUpload::Notifications::ManifestImportSuccessNotificationOverride if defined?(Katello)
@@ -136,15 +143,29 @@ module ForemanRhCloud
136
143
  Foreman::Gettext::Support.add_text_domain locale_domain, locale_dir
137
144
  end
138
145
 
139
- config.to_prepare do
146
+ initializer 'foreman_rh_cloud.register_rex_features', :before => :finisher_hook do |_app|
140
147
  # skip database manipulations while tables do not exist, like in migrations
141
- if ActiveRecord::Base.connection.data_source_exists?(ForemanTasks::Task.table_name) &&
148
+ if ActiveRecord::Base.connection.data_source_exists?(ForemanTasks::Task.table_name)
142
149
  RemoteExecutionFeature.register(
143
150
  :rh_cloud_remediate_hosts,
144
151
  N_('Apply Insights recommendations'),
145
152
  description: N_('Run remediation playbook generated by Insights'),
146
153
  host_action_button: false
147
154
  )
155
+ RemoteExecutionFeature.register(
156
+ :rh_cloud_connector_run_playbook,
157
+ N_('Run RH Cloud playbook'),
158
+ description: N_('Run playbook genrated by Red Hat remediations app'),
159
+ host_action_button: false,
160
+ provided_inputs: ['playbook_url', 'report_url', 'correlation_id', 'report_interval']
161
+ )
162
+ RemoteExecutionFeature.register(
163
+ :ansible_configure_cloud_connector,
164
+ N_('Configure Cloud Connector on given hosts'),
165
+ :description => N_('Configure Cloud Connector on given hosts'),
166
+ :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY
167
+ )
168
+
148
169
  # skip object creation when admin user is not present, for example in test DB
149
170
  if User.unscoped.find_by_login(User::ANONYMOUS_ADMIN).present?
150
171
  ::ForemanTasks.dynflow.config.on_init(false) do |world|
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '5.0.32'.freeze
2
+ VERSION = '5.0.35'.freeze
3
3
  end
@@ -121,4 +121,8 @@ module ForemanRhCloud
121
121
  def self.legacy_insights_ca
122
122
  "#{ForemanRhCloud::Engine.root}/config/rh_cert-api_chain.pem"
123
123
  end
124
+
125
+ def self.cloud_url_validator
126
+ @cloud_url_validator ||= Regexp.new(ENV['SATELLITE_RH_CLOUD_VALIDATOR'] || 'redhat.com$')
127
+ end
124
128
  end
@@ -0,0 +1,193 @@
1
+ module InsightsCloud
2
+ module Async
3
+ class ConnectorPlaybookExecutionReporterTask < ::Actions::EntryAction
4
+ include Dynflow::Action::Polling
5
+ include ForemanRhCloud::CloudAuth
6
+
7
+ def self.subscribe
8
+ Actions::RemoteExecution::RunHostsJob
9
+ end
10
+
11
+ def self.connector_feature_id
12
+ @connector_feature_id ||= RemoteExecutionFeature.feature!(:rh_cloud_connector_run_playbook).id
13
+ end
14
+
15
+ def plan(job_invocation)
16
+ return unless connector_playbook_job?(job_invocation)
17
+
18
+ @job_invocation = job_invocation
19
+
20
+ invocation_inputs = invocation_inputs(job_invocation)
21
+ report_url = invocation_inputs['report_url']
22
+ report_interval = [invocation_inputs['report_interval'].to_i, 5].max
23
+ correlation_id = invocation_inputs['correlation_id']
24
+
25
+ plan_self(
26
+ report_url: report_url,
27
+ report_interval: report_interval,
28
+ job_invocation_id: job_invocation.id,
29
+ correlation_id: correlation_id
30
+ )
31
+ end
32
+
33
+ def run(event = nil)
34
+ # Handle skip events
35
+ return if event == Dynflow::Action::Skip
36
+
37
+ super
38
+ end
39
+
40
+ def rescue_strategy_for_self
41
+ Dynflow::Action::Rescue::Skip
42
+ end
43
+
44
+ def done?(current_status = invocation_status)
45
+ job_invocation.finished? || current_status.map { |_host_id, task_status| task_status['report_done'] }.all?
46
+ end
47
+
48
+ # noop, we don't want to do anything when the polling task starts
49
+ def invoke_external_task
50
+ poll_external_task
51
+ end
52
+
53
+ def poll_external_task
54
+ current_status = invocation_status
55
+ report_job_progress(current_status)
56
+ # record the current state and increment the sequence for the next invocation
57
+ {
58
+ invocation_status: current_status,
59
+ }
60
+ end
61
+
62
+ def poll_intervals
63
+ [report_interval]
64
+ end
65
+
66
+ private
67
+
68
+ def connector_playbook_job?(job_invocation)
69
+ puts "Job invocation id: #{job_invocation&.remote_execution_feature_id}, feature id: #{connector_feature_id}"
70
+ job_invocation&.remote_execution_feature_id == connector_feature_id
71
+ end
72
+
73
+ def connector_feature_id
74
+ self.class.connector_feature_id
75
+ end
76
+
77
+ def invocation_inputs(job_invocation)
78
+ Hash[
79
+ job_invocation.pattern_template_invocations.first.input_values.map do |input_value|
80
+ [input_value.template_input.name, input_value.value]
81
+ end
82
+ ]
83
+ end
84
+
85
+ def job_invocation
86
+ @job_invocation ||= JobInvocation.find(input['job_invocation_id'])
87
+ end
88
+
89
+ def report_interval
90
+ @report_interval ||= input['report_interval']
91
+ end
92
+
93
+ def correlation_id
94
+ @correlation_id ||= input['correlation_id']
95
+ end
96
+
97
+ def host_status(host)
98
+ external_task&.dig('invocation_status', host)
99
+ end
100
+
101
+ def sequence(host)
102
+ host_status(host)&.fetch('sequence', 0).to_i
103
+ end
104
+
105
+ def host_done?(host)
106
+ ActiveModel::Type::Boolean.new.cast(host_status(host)&.fetch('report_done', nil))
107
+ end
108
+
109
+ def report_url
110
+ input['report_url']
111
+ end
112
+
113
+ def invocation_status
114
+ Hash[job_invocation.targeting.hosts.map do |host|
115
+ next unless host.insights&.uuid
116
+ [
117
+ host.insights.uuid,
118
+ task_status(job_invocation.sub_task_for_host(host), host.insights.uuid),
119
+ ]
120
+ end.compact]
121
+ end
122
+
123
+ def task_status(host_task, host_name)
124
+ unless host_task
125
+ return { 'state' => 'unknown' }
126
+ end
127
+
128
+ {
129
+ 'state' => host_task.state,
130
+ 'output' => host_task.main_action.live_output.map { |line| line['output'] }.join("\n"),
131
+ 'exit_status' => host_task.main_action.exit_status,
132
+ 'sequence' => sequence(host_name),
133
+ 'report_done' => host_done?(host_name),
134
+ }
135
+ end
136
+
137
+ def report_job_progress(invocation_status)
138
+ generator = InsightsCloud::Generators::PlaybookProgressGenerator.new(correlation_id)
139
+
140
+ invocation_status.each do |host_name, status|
141
+ # skip host if the host already reported that it's finished
142
+ next if status['report_done']
143
+
144
+ unless status['state'] == 'unknown'
145
+ sequence = status['sequence']
146
+ generator.host_progress_message(host_name, status['output'], sequence)
147
+ status['sequence'] = sequence + 1 # increase the sequence for each host
148
+ end
149
+
150
+ if status['state'] == 'stopped'
151
+ generator.host_finished_message(host_name, status['exit_status'])
152
+ status['report_done'] = true
153
+ end
154
+ end
155
+ generator.job_finished_message if done?(invocation_status)
156
+
157
+ send_report(generator.generate)
158
+ end
159
+
160
+ def send_report(report)
161
+ execute_cloud_request(
162
+ method: :post,
163
+ url: report_url,
164
+ content_type: 'application/vnd.redhat.playbook-sat.v3+jsonl',
165
+ payload: {
166
+ file: wrap_report(report),
167
+ multipart: true,
168
+ }
169
+ )
170
+ end
171
+
172
+ # RestClient has to accept an object that responds to :read, :path and :content_type methods
173
+ # to properly generate a multipart message.
174
+ # see: https://github.com/rest-client/rest-client/blob/2c72a2e77e2e87d25ff38feba0cf048d51bd5eca/lib/restclient/payload.rb#L161
175
+ def wrap_report(report)
176
+ obj = StringIO.new(report)
177
+ def obj.content_type
178
+ 'application/vnd.redhat.playbook-sat.v3+jsonl'
179
+ end
180
+
181
+ def obj.path
182
+ 'file'
183
+ end
184
+
185
+ obj
186
+ end
187
+
188
+ def logger
189
+ action_logger
190
+ end
191
+ end
192
+ end
193
+ end
@@ -22,6 +22,10 @@ module InsightsCloud
22
22
  def rescue_strategy_for_self
23
23
  Dynflow::Action::Rescue::Fail
24
24
  end
25
+
26
+ def logger
27
+ action_logger
28
+ end
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,49 @@
1
+ module InsightsCloud
2
+ module Generators
3
+ class PlaybookProgressGenerator
4
+ attr_reader :correlation_id
5
+ def initialize(correlation_id)
6
+ @messages = []
7
+ @correlation_id = correlation_id
8
+ end
9
+
10
+ def host_progress_message(host_name, output, sequence)
11
+ @messages << {
12
+ "type": "playbook_run_update",
13
+ "version": 3,
14
+ "correlation_id": correlation_id,
15
+ "sequence": sequence,
16
+ "host": host_name,
17
+ "console": output,
18
+ }
19
+ end
20
+
21
+ def host_finished_message(host_name, exit_code)
22
+ @messages << {
23
+ "type": "playbook_run_finished",
24
+ "version": 3,
25
+ "correlation_id": correlation_id,
26
+ "host": host_name,
27
+ "status": exit_code == 0 ? 'success' : 'failure',
28
+ "connection_code": 0,
29
+ "execution_code": exit_code,
30
+ }
31
+ end
32
+
33
+ def job_finished_message
34
+ @messages << {
35
+ "type": "playbook_run_completed",
36
+ "version": 3,
37
+ "correlation_id": correlation_id,
38
+ "status": "success",
39
+ }
40
+ end
41
+
42
+ def generate
43
+ @messages.map do |message|
44
+ message.to_json
45
+ end.join("\n")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -19,4 +19,17 @@ namespace :rh_cloud_insights do
19
19
 
20
20
  puts "Deleted #{deleted_count} insights statuses"
21
21
  end
22
+
23
+ desc "Re-announce all organizations into Sources on RH cloud."
24
+ task announce_to_sources: [:environment] do
25
+ logger = Logging::Logger.new(STDOUT)
26
+ Organization.unscoped.each do |org|
27
+ presence = ForemanRhCloud::CloudPresence.new(org, logger)
28
+ presence.announce_to_sources
29
+ rescue StandardError => ex
30
+ logger.warn(ex)
31
+ end
32
+
33
+ logger.info('Reannounced all organizations')
34
+ end
22
35
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "5.0.32",
3
+ "version": "5.0.35",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,78 @@
1
+ require 'json'
2
+ require 'test_plugin_helper'
3
+
4
+ module InsightsCloud::Api
5
+ class CloudRequestControllerTest < ActionController::TestCase
6
+ tests Api::V2::RhCloud::CloudRequestController
7
+
8
+ setup do
9
+ @test_org = FactoryBot.create(:organization)
10
+ end
11
+
12
+ test 'Fails for unknown directives' do
13
+ request_params = run_playbook_request
14
+ request_params['directive'] = 'not-valid'
15
+
16
+ post :update, params: request_params
17
+
18
+ assert_response :bad_request
19
+ end
20
+
21
+ test 'Starts playbook run for correct directive' do
22
+ Setting[:rh_cloud_token] = 'MOCK_TOKEN'
23
+ host1 = FactoryBot.create(:host, :with_insights_hits)
24
+ host1.insights.uuid = 'TEST_UUID1'
25
+ host1.insights.save!
26
+ host2 = FactoryBot.create(:host, :with_insights_hits)
27
+ host2.insights.uuid = 'TEST_UUID2'
28
+ host2.insights.save!
29
+
30
+ mock_composer = mock('composer')
31
+ ::JobInvocationComposer.expects(:for_feature).with do |feature, host_ids, params|
32
+ feature == :rh_cloud_connector_run_playbook &&
33
+ host_ids.first == host1.id &&
34
+ host_ids.last == host2.id
35
+ end.returns(mock_composer)
36
+ mock_composer.expects(:trigger!)
37
+ mock_composer.expects(:job_invocation)
38
+
39
+ post :update, params: run_playbook_request
40
+
41
+ assert_response :success
42
+ end
43
+
44
+ private
45
+
46
+ def run_playbook_request
47
+ request_json = <<-REQUEST
48
+ {
49
+ "type": "data",
50
+ "message_id": "a6a7d866-7de0-409a-84e0-3c56c4171bb7",
51
+ "version": 1,
52
+ "sent": "2021-01-12T15:30:08+00:00",
53
+ "directive": "playbook-sat",
54
+ "metadata": {
55
+ "operation": "run",
56
+ "return_url": "https://cloud.redhat.com/api/v1/ingres/upload",
57
+ "correlation_id": "6684b9dd-0d16-42c1-b13a-9f45be59e3b6",
58
+ "playbook_run_name": "Human-readable playbook run name",
59
+ "playbook_run_url": "https://console.redhat.com/insights/remediations/1234",
60
+ "sat_id": "aa3b1faa-56f3-4d14-8258-615d11e20060",
61
+ "sat_org_id": "#{FactoryBot.create(:organization).id}",
62
+ "initiator_user_id": "4efca34c6d9ae05ef7c3d7a7424e6370d198159a841ae005084888a9a4529e27",
63
+ "hosts": "TEST_UUID1,TEST_UUID2",
64
+ "response_interval": "30",
65
+ "response_full": "false"
66
+ },
67
+ "content": ""
68
+ }
69
+ REQUEST
70
+
71
+ request = JSON.parse(request_json)
72
+
73
+ request['content'] = "\"#{Base64.encode64('https://cloud.redhat.com/api/v1/remediations/1234/playbook')}\""
74
+
75
+ request
76
+ end
77
+ end
78
+ end
@@ -60,6 +60,18 @@ module InsightsCloud::Api
60
60
  assert_equal x_rh_insights_request_id, @response.headers['x_rh_insights_request_id']
61
61
  end
62
62
 
63
+ test "should set etag header to response from cloud" do
64
+ etag = '12345'
65
+ req = RestClient::Request.new(:method => 'GET', :url => 'http://test.theforeman.org', :headers => { "If-None-Match": etag})
66
+ net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
67
+ net_http_resp[Rack::ETAG] = etag
68
+ res = RestClient::Response.create(@body, net_http_resp, req)
69
+ ::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)
70
+
71
+ get :forward_request, params: { "path" => "static/v1/release/insights-core.egg" }
72
+ assert_equal etag, @response.headers[Rack::ETAG]
73
+ end
74
+
63
75
  test "should handle failed authentication to cloud" do
64
76
  net_http_resp = Net::HTTPResponse.new(1.0, 401, "Unauthorized")
65
77
  res = RestClient::Response.create(@body, net_http_resp, @http_req)
@@ -116,6 +128,10 @@ module InsightsCloud::Api
116
128
  test 'should get branch info' do
117
129
  get :branch_info
118
130
 
131
+ res = JSON.parse(@response.body)
132
+
133
+ assert_not_equal 0, res['labels'].find { |l| l['key'] == 'hostgroup' }.count
134
+
119
135
  assert_response :success
120
136
  end
121
137