foreman_remote_execution 3.3.0 → 3.3.5

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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +101 -0
  3. data/.rubocop_todo.yml +3 -0
  4. data/Gemfile +1 -0
  5. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +6 -5
  6. data/app/controllers/api/v2/job_invocations_controller.rb +23 -1
  7. data/app/controllers/api/v2/template_invocations_controller.rb +4 -1
  8. data/app/helpers/job_invocations_helper.rb +1 -1
  9. data/app/helpers/remote_execution_helper.rb +38 -33
  10. data/app/models/concerns/foreman_remote_execution/host_extensions.rb +1 -1
  11. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +18 -9
  12. data/app/services/default_proxy_proxy_selector.rb +3 -1
  13. data/app/views/api/v2/job_invocations/main.json.rabl +8 -2
  14. data/app/views/job_invocations/_card_target_hosts.html.erb +1 -1
  15. data/app/views/job_invocations/_tab_hosts.html.erb +3 -23
  16. data/app/views/job_invocations/index.html.erb +2 -1
  17. data/app/views/job_invocations/show.html.erb +3 -3
  18. data/app/views/job_invocations/show.js.erb +0 -18
  19. data/app/views/job_invocations/show.json.erb +4 -0
  20. data/app/views/templates/ssh/package_action.erb +1 -0
  21. data/app/views/templates/ssh/puppet_agent_disable.erb +3 -0
  22. data/app/views/templates/ssh/puppet_agent_enable.erb +3 -0
  23. data/app/views/templates/ssh/puppet_install_modules_from_forge.erb +3 -0
  24. data/app/views/templates/ssh/puppet_run_once.erb +3 -0
  25. data/foreman_remote_execution.gemspec +4 -5
  26. data/lib/foreman_remote_execution/version.rb +1 -1
  27. data/locale/action_names.rb +0 -1
  28. data/test/functional/api/v2/job_invocations_controller_test.rb +42 -14
  29. data/test/models/orchestration/ssh_test.rb +32 -0
  30. data/test/unit/concerns/host_extensions_test.rb +7 -0
  31. data/webpack/__mocks__/foremanReact/common/I18n.js +1 -0
  32. data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +3 -0
  33. data/webpack/__mocks__/foremanReact/constants.js +3 -0
  34. data/webpack/index.js +8 -5
  35. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +52 -0
  36. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +8 -0
  37. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
  38. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +12 -0
  39. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +6 -0
  40. data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +6 -0
  41. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +6 -0
  42. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +31 -0
  43. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +12 -0
  44. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +81 -0
  45. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +43 -0
  46. data/webpack/react_app/components/TargetingHosts/components/HostItem.js +39 -0
  47. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +54 -0
  48. data/webpack/react_app/components/TargetingHosts/index.js +37 -0
  49. metadata +26 -12
  50. data/.hound.yml +0 -14
  51. data/.travis.yml +0 -5
  52. data/app/views/job_invocations/_host_actions_td.html.erb +0 -3
  53. data/app/views/job_invocations/_host_name_td.html.erb +0 -8
  54. data/app/views/job_invocations/_host_status_td.html.erb +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d00f4722f74258ad947432c2d8104522d3fb53749f7bac4e7c52914862af3ecc
4
- data.tar.gz: 187645d51578339523b94fb83fe781cba865477a616c2d8c0980255be1fd0377
3
+ metadata.gz: 6add16b63c650250f993954c92620863d9904055d4b175fa76b04b97dd70ee92
4
+ data.tar.gz: 7fca35b0b17e7f2ad0f5769fae4f56b0d5e6f0c85b9ebcc8abab29858bf585a5
5
5
  SHA512:
6
- metadata.gz: 04c6a44e96bb03d75310d7dd48acd249503f04778c62f520d1101bf4719d195c2cd4d11e9a6ae63c97e24d8cbd50884dd090ec56a54aff933ee19f3379bbeec1
7
- data.tar.gz: 165ba49b54b18ed4128c7b72a53ef52fea60f6f9a619ee7bd0a22545910cf68997b763e27f3569892b20dd528e70236c44b241c1d1b6c5fcec15a5a21f76af7e
6
+ metadata.gz: 359e21b88b85d45b1c4faef9a7e841b0c34e008d3e818c111eba4079167683ff6c59e1354f2f2aafc18e7be43c93c00c368eb9d331636d6c3103d6c59f637306
7
+ data.tar.gz: 17aa0693af9ec50fca1a97e4e685c914a665edfe78cf0d9a87b17a9c683490ad7b151748c36adb9b9bcb50389afaca533065d4590d23f2b7550eddad8017538c
@@ -0,0 +1,101 @@
1
+ name: CI
2
+ on: [pull_request]
3
+ env:
4
+ RAILS_ENV: test
5
+ DATABASE_URL: postgresql://postgres:@localhost/test
6
+ DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true
7
+ jobs:
8
+ rubocop:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Setup Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6
16
+ - name: Setup
17
+ run: |
18
+ gem install bundler
19
+ bundle install --jobs=3 --retry=3
20
+ - name: Run rubocop
21
+ run: bundle exec rubocop
22
+ test_ruby:
23
+ runs-on: ubuntu-latest
24
+ needs: rubocop
25
+ services:
26
+ postgres:
27
+ image: postgres:12.1
28
+ ports: ['5432:5432']
29
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ foreman-core-branch: [2.1-stable, develop]
34
+ ruby-version: [2.5, 2.6]
35
+ node-version: [12]
36
+ steps:
37
+ - run: sudo apt-get update
38
+ - run: sudo apt-get install build-essential libcurl4-openssl-dev zlib1g-dev libpq-dev
39
+ - uses: actions/checkout@v2
40
+ with:
41
+ repository: theforeman/foreman
42
+ ref: ${{ matrix.foreman-core-branch }}
43
+ - uses: actions/checkout@v2
44
+ with:
45
+ path: foreman_remote_execution
46
+ - name: Setup Ruby
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ ruby-version: ${{ matrix.ruby-version }}
50
+ - name: Setup Node
51
+ uses: actions/setup-node@v1
52
+ with:
53
+ node-version: ${{ matrix.node-version }}
54
+ - uses: actions/cache@v1
55
+ with:
56
+ path: vendor/bundle
57
+ key: ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
58
+ restore-keys: |
59
+ ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-
60
+ - name: Setup Bundler
61
+ run: |
62
+ echo "gem 'foreman_remote_execution', path: './foreman_remote_execution'" > bundler.d/foreman_remote_execution.local.rb
63
+ gem install bundler
64
+ bundle config set without journald development console libvirt
65
+ bundle config set path vendor/bundle
66
+ - name: Prepare test env
67
+ run: |
68
+ bundle install --jobs=3 --retry=3
69
+ bundle exec rake db:create
70
+ bundle exec rake db:migrate
71
+ - name: Run plugin tests
72
+ run: |
73
+ bundle exec rake test:foreman_remote_execution
74
+ bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
75
+ test_js:
76
+ runs-on: ubuntu-latest
77
+ needs: rubocop
78
+ strategy:
79
+ fail-fast: false
80
+ matrix:
81
+ ruby-version: [2.6]
82
+ node-version: [10, 12]
83
+ steps:
84
+ - uses: actions/checkout@v2
85
+ - name: Setup Ruby
86
+ uses: ruby/setup-ruby@v1
87
+ with:
88
+ ruby-version: ${{ matrix.ruby-version }}
89
+ - name: Setup Node
90
+ uses: actions/setup-node@v1
91
+ with:
92
+ node-version: ${{ matrix.node-version }}
93
+ - name: Nmp install
94
+ run: |
95
+ npm install
96
+ - name: Run plugin linter
97
+ run: |
98
+ npm run lint
99
+ - name: Run plugin tests
100
+ run: |
101
+ npm run test
@@ -6,6 +6,9 @@
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ Minitest/GlobalExpectations:
10
+ Enabled: false
11
+
9
12
  # Offense count: 2
10
13
  # Cop supports --auto-correct.
11
14
  # Configuration parameters: TreatCommentsAsGroupSeparators, Include.
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec :name => 'foreman_remote_execution'
4
4
 
5
+ gem 'rubocop', '~> 0.80.0'
5
6
  gem 'rubocop-minitest'
6
7
  gem 'rubocop-performance'
7
8
  gem 'rubocop-rails'
@@ -1,9 +1,6 @@
1
- div.infoblock {
2
- margin-bottom: 20px;
3
- line-height: 2;
4
-
1
+ .target-hosts-card {
5
2
  pre {
6
- margin-top: 5px;
3
+ white-space:pre-line;
7
4
  }
8
5
  }
9
6
 
@@ -29,3 +26,7 @@ div.infoblock {
29
26
  }
30
27
  }
31
28
  }
29
+
30
+ .text_warp{
31
+ word-wrap: break-word;
32
+ }
@@ -18,7 +18,17 @@ module Api
18
18
 
19
19
  api :GET, '/job_invocations/:id', N_('Show job invocation')
20
20
  param :id, :identifier, :required => true
21
+ param :host_status, :bool, required: false, desc: N_('Show Job status for the hosts')
21
22
  def show
23
+ @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
24
+ @template_invocations = @job_invocation.template_invocations
25
+ .where(host: @hosts)
26
+ .includes(:input_values)
27
+
28
+ if params[:host_status] == 'true'
29
+ template_invocations = @template_invocations.includes(:run_host_job_task).to_a
30
+ @host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
31
+ end
22
32
  end
23
33
 
24
34
  def_param_group :job_invocation do
@@ -70,6 +80,7 @@ module Api
70
80
  end
71
81
  composer.trigger!
72
82
  @job_invocation = composer.job_invocation
83
+ @hosts = @job_invocation.targeting.hosts
73
84
  process_response @job_invocation
74
85
  end
75
86
 
@@ -146,7 +157,7 @@ module Api
146
157
  end
147
158
 
148
159
  def find_host
149
- @host = Host.authorized(:view_hosts).find(params['host_id'])
160
+ @host = @nested_obj.targeting.hosts.authorized(:view_hosts, Host).find(params['host_id'])
150
161
  rescue ActiveRecord::RecordNotFound
151
162
  not_found({ :error => { :message => (_("Host with id '%{id}' was not found") % { :id => params['host_id'] }) } })
152
163
  end
@@ -204,6 +215,17 @@ module Api
204
215
  def parent_scope
205
216
  resource_class.where(nil)
206
217
  end
218
+
219
+ def template_invocation_status(template_invocation)
220
+ task = template_invocation.try(:run_host_job_task)
221
+ parent_task = @job_invocation.task
222
+
223
+ return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
224
+ return task.state if task.state == 'running' || task.state == 'planned'
225
+ return 'error' if task.result == 'warning'
226
+
227
+ task.result
228
+ end
207
229
  end
208
230
  end
209
231
  end
@@ -26,7 +26,10 @@ module Api
26
26
  private
27
27
 
28
28
  def resource_scope_for_template_invocations
29
- @job_invocation.template_invocations.search_for(*search_options)
29
+ @job_invocation.template_invocations
30
+ .includes(:host)
31
+ .where(host: Host.authorized(:view_hosts, Host))
32
+ .search_for(*search_options)
30
33
  end
31
34
 
32
35
  def find_job_invocation
@@ -29,7 +29,7 @@ module JobInvocationsHelper
29
29
  end
30
30
 
31
31
  def preview_hosts(template_invocation)
32
- hosts = template_invocation.targeting.hosts.take(20)
32
+ hosts = template_invocation.targeting.hosts.authorized(:view_hosts, Host).take(20)
33
33
  hosts.map do |host|
34
34
  collapsed_preview(host) +
35
35
  render(:partial => 'job_invocations/user_input',
@@ -14,43 +14,35 @@ module RemoteExecutionHelper
14
14
  end
15
15
 
16
16
  def template_invocation_status(task, parent_task)
17
- if task.nil?
18
- if parent_task.result == 'cancelled'
19
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
20
- else
21
- icon_text('question', 'N/A', :kind => 'fa')
22
- end
23
- elsif task.state == 'running'
24
- icon_text('running', _('running'), :kind => 'pficon')
25
- elsif task.state == 'planned'
26
- icon_text('build', _('planned'), :kind => 'pficon')
27
- else
28
- case task.result
29
- when 'warning', 'error'
30
- icon_text('error-circle-o', _('failed'), :kind => 'pficon')
31
- when 'cancelled'
32
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
33
- when 'success'
34
- icon_text('ok', _('success'), :kind => 'pficon')
35
- else
36
- task.result
37
- end
38
- end
17
+ return(parent_task.result == 'cancelled' ? _('cancelled') : 'N/A') if task.nil?
18
+ return task.state if task.state == 'running' || task.state == 'planned'
19
+ return _('error') if task.result == 'warning'
20
+
21
+ task.result
39
22
  end
40
23
 
41
24
  def template_invocation_actions(task, host, job_invocation, template_invocation)
25
+ links = []
42
26
  host_task = template_invocation.try(:run_host_job_task)
43
- [
44
- display_link_if_authorized(_('Host detail'), hash_for_host_path(host).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer)),
45
- display_link_if_authorized(_('Rerun on %s') % host.name, hash_for_rerun_job_invocation_path(:id => job_invocation, :host_ids => [ host.id ], :authorizer => job_hosts_authorizer)),
46
- if host_task.present?
47
- display_link_if_authorized(
48
- _('Host task'),
49
- hash_for_foreman_tasks_task_path(host_task)
50
- .merge(:auth_object => host_task, :permission => :view_foreman_tasks)
51
- )
52
- end,
53
- ]
27
+
28
+ if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
29
+ links << { title: _('Host detail'),
30
+ action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
31
+ end
32
+
33
+ if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
34
+ links << { title: (_('Rerun on %s') % host.name),
35
+ action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
36
+ 'data-method': 'get', id: "#{host.name}-actions-rerun" } }
37
+ end
38
+
39
+ if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
40
+ links << { title: _('Host task'),
41
+ action: { href: foreman_tasks_task_path(host_task),
42
+ 'data-method': 'get', id: "#{host.name}-actions-task" } }
43
+ end
44
+
45
+ links
54
46
  end
55
47
 
56
48
  def remote_execution_provider_for(template_invocation)
@@ -237,4 +229,17 @@ module RemoteExecutionHelper
237
229
 
238
230
  task.execution_plan.actions[1].try(:input).try(:[], 'script')
239
231
  end
232
+
233
+ def targeting_hosts(job_invocation, hosts)
234
+ hosts.map do |host|
235
+ template_invocation = job_invocation.template_invocations.find { |template_inv| template_inv.host_id == host.id }
236
+ task = template_invocation.try(:run_host_job_task)
237
+ link_authorized = !task.nil? && authorized_for(hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer))
238
+
239
+ { name: host.name,
240
+ link: link_authorized ? template_invocation_path(:id => template_invocation) : '',
241
+ status: template_invocation_status(task, job_invocation.task),
242
+ actions: template_invocation_actions(task, host, job_invocation, template_invocation) }
243
+ end
244
+ end
240
245
  end
@@ -49,7 +49,7 @@ module ForemanRemoteExecution
49
49
  keys = remote_execution_ssh_keys
50
50
  source = 'global'
51
51
  if keys.present?
52
- value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| v || [] }
52
+ value, safe_value = params.fetch('remote_execution_ssh_keys', {}).values_at(:value, :safe_value).map { |v| [v].flatten.compact }
53
53
  params['remote_execution_ssh_keys'] = {:value => value + keys, :safe_value => safe_value + keys, :source => source}
54
54
  end
55
55
  [:remote_execution_ssh_user, :remote_execution_effective_user_method,
@@ -8,14 +8,20 @@ module ForemanRemoteExecution
8
8
  register_rebuild(:queue_ssh_destroy, N_("SSH_#{self.to_s.split('::').first}"))
9
9
  end
10
10
 
11
- def drop_from_known_hosts(args)
12
- proxy_id, target = args
11
+ def drop_from_known_hosts(proxy_id)
12
+ _, _, target = host_kind_target
13
+ return true if target.nil?
14
+
13
15
  proxy = ::SmartProxy.find(proxy_id)
14
16
  begin
15
17
  proxy.drop_host_from_known_hosts(target)
16
- rescue RestClient::ResourceNotFound => e
17
- # ignore 404 when known_hosts entry is missing or the module was not enabled
18
- Foreman::Logging.exception "Proxy failed to delete SSH known_hosts for #{name}, #{ip}", e, :level => :error
18
+ rescue ::ProxyAPI::ProxyException => e
19
+ if e.wrapped_exception.is_a?(RestClient::NotFound)
20
+ # ignore 404 when known_hosts entry is missing or the module was not enabled
21
+ Foreman::Logging.exception "Proxy failed to delete SSH known_hosts for #{name}, #{ip}", e, :level => :error
22
+ else
23
+ raise e
24
+ end
19
25
  rescue => e
20
26
  Rails.logger.warn e.message
21
27
  return false
@@ -26,11 +32,14 @@ module ForemanRemoteExecution
26
32
  def ssh_destroy
27
33
  logger.debug "Scheduling SSH known_hosts cleanup"
28
34
 
29
- host, _kind, target = host_kind_target
30
- proxies = host.remote_execution_proxies('SSH').values
35
+ host, _kind, _target = host_kind_target
36
+ # #remote_execution_proxies may not be defined on the host object in some case
37
+ # for example Host::Discovered does not have it defined, even though these hosts
38
+ # have Nic::Managed interfaces associated with them
39
+ proxies = (host.try(:remote_execution_proxies, 'SSH') || {}).values
31
40
  proxies.flatten.uniq.each do |proxy|
32
41
  queue.create(id: queue_id(proxy.id), name: _("Remove SSH known hosts for %s") % self,
33
- priority: 200, action: [self, :drop_from_known_hosts, [proxy.id, target]])
42
+ priority: 200, action: [self, :drop_from_known_hosts, proxy.id])
34
43
  end
35
44
  end
36
45
 
@@ -40,7 +49,7 @@ module ForemanRemoteExecution
40
49
 
41
50
  def should_drop_from_known_hosts?
42
51
  host, = host_kind_target
43
- host&.build && host&.changes&.key?('build')
52
+ host && !host.new_record? && host.build && host.changes.key?('build')
44
53
  end
45
54
 
46
55
  private
@@ -10,7 +10,9 @@ class DefaultProxyProxySelector < ::RemoteExecutionProxySelector
10
10
  def available_proxies(host, provider)
11
11
  # TODO: Once we have a internal proxy marker/feature on the proxy, we can
12
12
  # swap the implementation
13
- internal_proxy = ::Katello.default_capsule
13
+ raise _('default_capsule method missing from SmartProxy') unless ::SmartProxy.respond_to?(:default_capsule)
14
+
15
+ internal_proxy = ::SmartProxy.default_capsule
14
16
  super.reduce({}) do |acc, (key, proxies)|
15
17
  acc.merge(key => proxies.select { |proxy| proxy == internal_proxy })
16
18
  end
@@ -19,8 +19,14 @@ child :targeting do
19
19
  attributes :bookmark_id, :search_query, :targeting_type, :user_id, :status, :status_label,
20
20
  :randomized_ordering
21
21
 
22
- child :hosts do
22
+ child @hosts do
23
23
  extends 'api/v2/hosts/base'
24
+
25
+ if params[:host_status] == 'true'
26
+ node :job_status do |host|
27
+ @host_statuses[host.id]
28
+ end
29
+ end
24
30
  end
25
31
  end
26
32
 
@@ -28,7 +34,7 @@ child :task do
28
34
  attributes :id, :state
29
35
  end
30
36
 
31
- child :template_invocations do
37
+ child @template_invocations do
32
38
  attributes :template_id, :template_name
33
39
  child :input_values do
34
40
  attributes :template_input_name, :template_input_id
@@ -1,5 +1,5 @@
1
1
  <% template_invocations = job_invocation.pattern_template_invocations %>
2
- <div class="card-pf card-pf-accented">
2
+ <div class="card-pf card-pf-accented target-hosts-card">
3
3
  <div class="card-pf-title">
4
4
  <h2 style="height: 18px;" class="card-pf-title">
5
5
  <%= _('Target hosts') %>
@@ -15,29 +15,9 @@
15
15
  <% end %>
16
16
  <br>
17
17
 
18
- <table class="<%= table_css_classes('table-condensed') %>">
19
- <thead>
20
- <tr>
21
- <th><%= sort :name, :as => _('Host') %></th>
22
- <th><%= _('Status') %></th>
23
- <th><%= _('Actions') %></th>
24
- </tr>
25
- </thead>
26
-
27
- <tbody>
28
- <% hosts.each do |host| %>
29
- <% template_invocation = job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
30
- <% task = template_invocation.try(:run_host_job_task) %>
31
- <tr>
32
- <% options = { :host => host, :task => task, :job_invocation => job_invocation, :template_invocation => template_invocation } %>
33
- <%= render 'host_name_td', options %>
34
- <%= render 'host_status_td', options %>
35
- <%= render 'host_actions_td', options %>
36
- </tr>
37
- <% end %>
38
- </tbody>
39
- </table>
40
-
18
+ <div id="targeting_hosts">
19
+ <%= mount_react_component('TargetingHosts', '#targeting_hosts') %>
20
+ </div>
41
21
  <%= will_paginate_with_info @hosts, :container => true %>
42
22
  <% else %>
43
23
  <div class="alert alert-warning">