foreman_wreckingball 3.2.0 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/foreman_wreckingball/modal.js +26 -17
  3. data/app/assets/javascripts/foreman_wreckingball/status_hosts_table.js +34 -2
  4. data/app/assets/javascripts/foreman_wreckingball/status_managed_hosts_dashboard.js +8 -0
  5. data/app/assets/stylesheets/foreman_wreckingball/status_managed_hosts_dashboard.css.scss +8 -0
  6. data/app/controllers/foreman_wreckingball/hosts_controller.rb +47 -31
  7. data/app/helpers/foreman_wreckingball/statuses_helper.rb +21 -0
  8. data/app/lib/actions/foreman_wreckingball/bulk_remediate.rb +27 -0
  9. data/app/models/concerns/foreman_wreckingball/host_status_extensions.rb +1 -1
  10. data/app/views/foreman_wreckingball/hosts/_hosts.json.rabl +5 -11
  11. data/app/views/foreman_wreckingball/hosts/_status_dashboard_content.erb +2 -0
  12. data/app/views/foreman_wreckingball/hosts/_status_managed_hosts_dashboard_cards.html.erb +16 -0
  13. data/app/views/foreman_wreckingball/hosts/_status_managed_hosts_dashboard_cards_card.html.erb +11 -0
  14. data/app/views/foreman_wreckingball/hosts/_status_row.html.erb +8 -12
  15. data/app/views/foreman_wreckingball/hosts/_status_row_actions.html.erb +22 -0
  16. data/app/views/foreman_wreckingball/hosts/_status_row_hosts_table.html.erb +7 -0
  17. data/app/views/foreman_wreckingball/hosts/_status_row_hosts_table_actions.html.erb +9 -0
  18. data/app/views/foreman_wreckingball/hosts/schedule_remediate.html.erb +44 -26
  19. data/app/views/foreman_wreckingball/hosts/status_dashboard.html.erb +6 -6
  20. data/app/views/foreman_wreckingball/hosts/status_managed_hosts_dashboard.html.erb +81 -30
  21. data/config/routes.rb +2 -4
  22. data/lib/foreman_wreckingball/version.rb +1 -1
  23. data/test/actions/foreman_wreckingball/bulk_remediate_test.rb +31 -0
  24. data/test/controllers/foreman_wreckingball/hosts_controller_test.rb +83 -29
  25. data/test/helpers/foreman_wreckingball/status_helper.rb +10 -0
  26. data/test/integration/hosts_status_managed_hosts_test.rb +1 -1
  27. data/test/test_plugin_helper.rb +2 -0
  28. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de6be68d909a39447b86fc7b8ce9f372cc12c929364910205beac6d8dc244829
4
- data.tar.gz: 2908a912c935764b978d0f094306ecbad458c70c9bed5138051f52d73f83697f
3
+ metadata.gz: 5038fc9d12985cb5fb7ee83a5dc72567d63a04fc11ff5d8d17c09d4cc59ea6b9
4
+ data.tar.gz: 1579d2bb8bb7c35188b76861b84611a190103fdef76f3135f2b3776d86a34003
5
5
  SHA512:
6
- metadata.gz: d4de761155c5e670000022056211ad3f4064a303914b869c8624f40679b723e6d90d7aa2ba3f156c9f2b535a02a9474cf8c817b7e967f537e7ca8e843a764a84
7
- data.tar.gz: 84e82bf20310a20cde12e50dcacd5fb84ab638e1eabf280aefbf3f1cb3d9a1253cbb57136dbb29ea618e95425380cd296a011f8dc40738b7339837833fd5bfa5
6
+ metadata.gz: ca6aefe61f1d908461e3f527b8384046923cbe3ecc20864165394532b4735587d1bab362836d2ae044c1c3b1a4d4e48e433bbeec34c7beaba18d20c62cc9f4bf
7
+ data.tar.gz: cfe558ecd67daf5a1b76943396df76626461023f80bae31662aad1cdbc6e4786206ee0307879b5e4d4725b9ecf75fa8a963bbf1f0f98dd7995e3bbe29330a653
@@ -9,29 +9,38 @@ function submit_modal_form() {
9
9
  $('#confirmation-modal').modal('hide');
10
10
  }
11
11
 
12
- function show_modal(element, url) {
13
- if (!url) {
14
- url = $(element).attr('href');
15
- }
16
- var title = $(element).attr('data-title');
17
- if (title) {
18
- $('#confirmation-modal .modal-title').text(title);
19
- }
12
+ function show_modal(element) {
13
+ if ($(element).attr('disabled')) { return; }
14
+
15
+ const url = $(element).attr('href');
16
+ const title = $(element).data('title');
17
+ const hostAssociation = $(element).data('host-association');
18
+
19
+ if (title) { $('#confirmation-modal .modal-title').text(title); }
20
+
20
21
  $('#confirmation-modal .modal-body')
21
22
  .empty()
22
23
  .append('<div class="modal-spinner spinner spinner-lg"></div>');
23
24
  $('#confirmation-modal').modal();
24
- $('#confirmation-modal .modal-body').load(url + ' #content',
25
- function(response, status, xhr) {
26
- $('#confirmation-modal .form-actions').remove();
27
25
 
28
- var submit_button = $('#confirmation-modal button[data-action="submit"]');
29
- if ($(element).attr('data-submit-class')) {
30
- submit_button.attr('class', 'btn ' + $(element).attr('data-submit-class'));
31
- } else {
32
- submit_button.attr('class', 'btn btn-primary');
33
- }
26
+ let params;
27
+ if (!!hostAssociation) {
28
+ params = Object.assign({}, { host_association: hostAssociation },
29
+ !!$(element).data('owned-only') ? { owned_only: true } : null);
30
+ } else if($(element).data('status-id')) {
31
+ params = { status_ids: [$(element).data('status-id')] };
32
+ } else {
33
+ const statusIds = $(element).closest('.status-row')
34
+ .find(':checkbox[name="status-id"]:checked')
35
+ .map((_, e) => { return e.value; })
36
+ .toArray();
37
+ params = { status_ids: statusIds };
38
+ }
34
39
 
40
+ $('#confirmation-modal .modal-body').load(`${url}?${$.param(params)} #content`,
41
+ (response, status, xhr) => {
42
+ $('#confirmation-modal .form-actions').remove();
43
+ $('#confirmation-modal button[data-action="submit"]').attr('class', 'btn btn-primary');
35
44
  $('#confirmation-modal a[rel="popover"]').popover();
36
45
 
37
46
  trigger_form_selector_binds('schedule_remediate_form', url);
@@ -10,7 +10,8 @@ $(document).ready(() => {
10
10
  processing: true,
11
11
  serverSide: true,
12
12
  columnDefs: [
13
- { className: 'ellipsis', targets: [0, 1, 2, 3] }
13
+ { className: 'ellipsis', targets: [1, 2, 3, 4] },
14
+ { width: '10px', targets: 0 }
14
15
  ],
15
16
  ajax: {
16
17
  url: $(element).data('hosts-url'),
@@ -26,13 +27,23 @@ $(document).ready(() => {
26
27
  dataSrc: (json) => {
27
28
  return json.data.map((e) => {
28
29
  let row = [
30
+ `<input name='status-id' type='checkbox' value='${e.status.id}' data-remediate='${!!e.remediate}' >`,
29
31
  `<a href="${e.path}">${e.name}</a>`,
30
32
  `<span class="${e.status.icon_class}"></span><span class="${e.status.status_class}">${e.status.label}</span>`,
31
33
  (e.owner || {}).name || '',
32
34
  (e.environment || {}).name || ''
33
35
  ];
34
36
  if(e.remediate) {
35
- row.push(`<span class="btn btn-sm btn-default"><a data-title="${e.remediate.title}" data-submit-class="btn-danger" onclick="show_modal(this); return false;" data-id="aid_wreckingball_hosts_${e.name}_schedule_remediate" href="${e.remediate.path}">${e.remediate.label}</a></span>`);
37
+ row.push(`
38
+ <span class="btn btn-sm btn-default">
39
+ <a data-title="${e.remediate.title}"
40
+ data-id="aid_wreckingball_hosts_${e.name}_schedule_remediate"
41
+ data-status-id="${e.status.id}"
42
+ onclick="show_modal(this); return false;"
43
+ href="${e.remediate.path}">
44
+ ${e.remediate.label}
45
+ </a>
46
+ </span>`);
36
47
  } else {
37
48
  row.push(null);
38
49
  }
@@ -44,6 +55,27 @@ $(document).ready(() => {
44
55
  $(element).on('error.dt', (_, settings) => {
45
56
  $(settings.nTable).closest('.status-hosts-container').addClass('ajax-error');
46
57
  });
58
+ $(element).on('page.dt', () => {
59
+ $selectAll.prop('checked', false).change();
60
+ $remediateSelected.attr('disabled', true);
61
+ });
62
+
63
+ const $selectAll = $(this).find(':checkbox[name="select-all"]');
64
+ const $remediateSelected = $(this).find('a.remediate-selected');
65
+
66
+ $selectAll.prop('checked', false);
67
+
68
+ $selectAll.change(() => {
69
+ const $selectHost = $(this).find(':checkbox[name="status-id"]');
70
+ $selectHost.prop('checked', $selectAll.is(':checked'));
71
+ $remediateSelected.attr('disabled', !$selectAll.is(':checked'));
72
+ });
73
+
74
+ $(this).on('change', ':checkbox[name=status-id]', () => {
75
+ const isAnyHostChecked = $(this).find(':checkbox[name="status-id"]').is(':checked');
76
+ $selectAll.prop('checked', isAnyHostChecked);
77
+ $remediateSelected.attr('disabled', !isAnyHostChecked);
78
+ });
47
79
  });
48
80
  });
49
81
  });
@@ -0,0 +1,8 @@
1
+ $(document).ready(() => {
2
+ $('table#missing_vms').add('table#duplicate_vms').add('table#different_vms')
3
+ .DataTable({
4
+ bLengthChange: true,
5
+ lengthMenu: [20, 50, 100],
6
+ order: [[ 0, 'desc' ]]
7
+ });
8
+ });
@@ -0,0 +1,8 @@
1
+ .status-managed-hosts-dashboard-cards {
2
+ .card-pf.card-pf-accented.card-pf-aggregate-status {
3
+ min-height: 100px;
4
+ .card-pf-title {
5
+ min-height: 30px;
6
+ }
7
+ }
8
+ }
@@ -12,8 +12,7 @@ module ForemanWreckingball
12
12
 
13
13
  AJAX_REQUESTS = [:status_hosts].freeze
14
14
  before_action :ajax_request, :only => AJAX_REQUESTS
15
- before_action :find_resource, :only => [:submit_remediate, :schedule_remediate]
16
- before_action :find_status, :only => [:submit_remediate, :schedule_remediate]
15
+ before_action :find_statuses, :only => [:schedule_remediate, :submit_remediate]
17
16
 
18
17
  def status_dashboard
19
18
  @newest_data = Host.authorized(:view_hosts).joins(:vmware_facet).maximum('vmware_facets.updated_at')
@@ -43,27 +42,24 @@ module ForemanWreckingball
43
42
  end
44
43
 
45
44
  def status_managed_hosts_dashboard
46
- @hosts = Host::Managed.authorized(:view_hosts, Host)
47
- .try { |query| params[:owned_only] ? query.owned_by_current_user_or_group_with_current_user : query }
48
-
49
- compute_resources = ComputeResource.where(:type => 'Foreman::Model::Vmware')
50
-
51
- # get all vms by compute resource id
52
- vms_by_compute_resource_id = {}
53
45
  # NOTE The call to ComputeResource#vms may slow things down
54
- compute_resources.each { |cr| vms_by_compute_resource_id[cr.id] = cr.vms(eager_loading: true) }
55
-
56
- vms_by_uuid = vms_by_compute_resource_id.values.flatten.group_by(&:uuid)
46
+ vms_by_compute_resource_id = Foreman::Model::Vmware.all.each_with_object({}) do |cr, memo|
47
+ memo[cr.id] = cr.vms(eager_loading: true)
48
+ end
57
49
 
58
50
  # Find all hosts with duplicate VMs
59
- @duplicate_vms = vms_by_uuid.select { |_uuid, vms| vms.size > 1 }
51
+ @duplicate_vms = vms_by_compute_resource_id.values
52
+ .flatten
53
+ .group_by(&:uuid)
54
+ .select { |_uuid, vms| vms.size > 1 }
60
55
 
61
56
  @missing_hosts = []
62
57
  @different_hosts = []
63
58
 
64
- @hosts.each do |host|
65
- next unless host.compute_resource_id
66
-
59
+ Host::Managed.authorized(:view_hosts, Host)
60
+ .where.not(compute_resource_id: nil)
61
+ .try { |query| params[:owned_only] ? query.owned_by_current_user_or_group_with_current_user : query }
62
+ .each do |host|
67
63
  # find the compute resource id of the host in the vm map
68
64
  cr_id, _vms = vms_by_compute_resource_id.find { |_cr_id, vms| vms.find { |vm| vm.uuid == host.uuid } }
69
65
 
@@ -79,7 +75,7 @@ module ForemanWreckingball
79
75
 
80
76
  # ajax method
81
77
  def status_hosts
82
- @status = HostStatus.find_wreckingball_status_by_host_association(params.fetch(:status).to_sym)
78
+ @status = HostStatus.find_wreckingball_status_by_host_association(params.fetch(:status))
83
79
 
84
80
  all_hosts = Host.authorized(:view_hosts, Host)
85
81
  .joins(@status.host_association)
@@ -119,20 +115,21 @@ module ForemanWreckingball
119
115
  end
120
116
 
121
117
  def submit_remediate
122
- raise Foreman::Exception, _('VMware Status can not be remediated.') unless @status.class.respond_to?(:supports_remediate?) && @status.class.supports_remediate?
123
- task = User.as_anonymous_admin do
124
- triggering = ::ForemanTasks::Triggering.new_from_params(triggering_params)
125
- if triggering.future?
126
- triggering.parse_start_at!
127
- triggering.parse_start_before!
128
- else
129
- triggering.start_at ||= Time.zone.now
130
- end
118
+ return not_found unless @statuses.any?
119
+
120
+ triggering = ::ForemanTasks::Triggering.new_from_params(triggering_params)
121
+ if triggering.future?
122
+ triggering.parse_start_at!
123
+ triggering.parse_start_before!
124
+ else
125
+ triggering.start_at ||= Time.zone.now
126
+ end
131
127
 
132
- triggering.trigger(@status.class.remediate_action, @host)
128
+ task = User.as_anonymous_admin do
129
+ triggering.trigger(::Actions::ForemanWreckingball::BulkRemediate, @statuses)
133
130
  end
134
- flash[:success] = _('Remediate VM task for %s was successfully scheduled.') % @host
135
- redirect_to(foreman_tasks_task_path(task.id))
131
+
132
+ redirect_to foreman_tasks_task_path(task.id)
136
133
  end
137
134
 
138
135
  private
@@ -143,8 +140,27 @@ module ForemanWreckingball
143
140
  end
144
141
  end
145
142
 
146
- def find_status
147
- @status = HostStatus::Status.find_by!(:id => params[:status_id], :host_id => @host.id)
143
+ def statuses_params
144
+ @statuses_params ||= params.permit(:host_association, :owned_only, status_ids: [])
145
+ end
146
+
147
+ def find_statuses
148
+ @statuses = begin
149
+ host_association = statuses_params[:host_association]
150
+ status_class = HostStatus.find_wreckingball_status_by_host_association(host_association)
151
+ if status_class
152
+ Host.authorized(:remediate_vmware_status_hosts, Host)
153
+ .joins(status_class.host_association)
154
+ .includes(status_class.host_association)
155
+ .try { |query| statuses_params[:owned_only] ? query.owned_by_current_user_or_group_with_current_user : query }
156
+ .where.not('host_status.status': status_class.global_ok_list)
157
+ .map { |host| host.send(status_class.host_association) }
158
+ else
159
+ HostStatus::Status.includes(:host).where(id: statuses_params[:status_ids]).select do |status|
160
+ User.current.can?(:remediate_vmware_status_hosts, status.host)
161
+ end
162
+ end
163
+ end
148
164
  end
149
165
 
150
166
  def action_permission
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWreckingball
4
+ module StatusesHelper
5
+ def status_actions(host_association, owned_only, supports_remediate)
6
+ actions = []
7
+ actions << display_link_if_authorized(_('Refresh'),
8
+ hash_for_refresh_status_dashboard_hosts_path,
9
+ title: _('Refresh Data'),
10
+ method: :put)
11
+ if supports_remediate
12
+ actions << display_link_if_authorized(_('Remediate All'),
13
+ hash_for_schedule_remediate_hosts_path,
14
+ 'data-host-association': host_association,
15
+ 'data-owned-only': owned_only,
16
+ onClick: 'show_modal(this); return false;')
17
+ end
18
+ actions.reject(&:blank?)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Actions
4
+ module ForemanWreckingball
5
+ class BulkRemediate < Actions::Base
6
+ def plan(statuses)
7
+ sequence do
8
+ statuses.group_by(&:class).each do |statuses_klass, statuses_list|
9
+ plan_action(::Actions::BulkAction, statuses_klass.remediate_action, statuses_list.map(&:host)) if statuses_klass.respond_to?(:remediate_action)
10
+ end
11
+ end
12
+ end
13
+
14
+ def run
15
+ # dummy run phase to save input
16
+ end
17
+
18
+ def resource_locks
19
+ :link
20
+ end
21
+
22
+ def humanized_name
23
+ _('Bulk remediate')
24
+ end
25
+ end
26
+ end
27
+ end
@@ -7,7 +7,7 @@ module ForemanWreckingball
7
7
  end
8
8
 
9
9
  def find_wreckingball_status_by_host_association(host_association)
10
- wreckingball_statuses.find { |s| s.host_association == host_association }
10
+ wreckingball_statuses.find { |s| s.host_association.to_s == host_association.to_s }
11
11
  end
12
12
  end
13
13
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  collection @hosts
4
4
 
5
- attributes :name
5
+ attributes :id, :name
6
6
 
7
7
  child owner: :owner do
8
8
  attribute :name
@@ -17,6 +17,7 @@ node(:path) { |host| host_path(host) }
17
17
  node(:status) do |host|
18
18
  status = host.public_send(locals[:host_association])
19
19
  {
20
+ id: status.id,
20
21
  label: status.to_label,
21
22
  icon_class: host_global_status_icon_class(status.to_global),
22
23
  status_class: host_global_status_class(status.to_global)
@@ -24,18 +25,11 @@ node(:status) do |host|
24
25
  end
25
26
 
26
27
  node(:remediate, if: lambda do |host|
27
- locals[:supports_remediate] && begin
28
- options = hash_for_schedule_remediate_host_path(id: host,
29
- status_id: host.public_send(locals[:host_association]).id)
30
- .merge(auth_object: host,
31
- permission: :remediate_vmware_status_hosts)
32
- authorized_for(options)
33
- end
34
- end) do |host|
35
- status_id = host.public_send(locals[:host_association]).id
28
+ locals[:supports_remediate] && User.current.can?(:remediate_vmware_status_hosts, host)
29
+ end) do
36
30
  {
37
31
  label: _('Remediate'),
38
32
  title: _('Remediate Host OS'),
39
- path: schedule_remediate_host_path(host, status_id: status_id)
33
+ path: schedule_remediate_hosts_path
40
34
  }
41
35
  end
@@ -24,6 +24,8 @@
24
24
  counter: status[:counter],
25
25
  status: status[:host_association],
26
26
  supports_remediate: status[:supports_remediate],
27
+ host_association: status[:host_association],
28
+ owned_only: params[:owned_only],
27
29
  id: idx
28
30
  }
29
31
  %>
@@ -0,0 +1,16 @@
1
+ <div class='status-managed-hosts-dashboard-cards container-fluid container-cards-pf'>
2
+ <div class='row row-cards-pf'>
3
+ <%= render partial: 'status_managed_hosts_dashboard_cards_card', locals: {
4
+ title: _('List of Hosts not found in vSphere'),
5
+ count: missing_hosts_count
6
+ } %>
7
+ <%= render partial: 'status_managed_hosts_dashboard_cards_card', locals: {
8
+ title: _('List of VMs with same uuid'),
9
+ count: duplicate_vms_count
10
+ } %>
11
+ <%= render partial: 'status_managed_hosts_dashboard_cards_card', locals: {
12
+ title: _('List of VMs associated with different compute resources'),
13
+ count: different_hosts_count
14
+ } %>
15
+ </div>
16
+ </div>
@@ -0,0 +1,11 @@
1
+ <div class='col-xs-12 col-sm-4'>
2
+ <div class='card-pf card-pf-accented card-pf-aggregate-status'>
3
+ <%= content_tag :h2, title, class: 'card-pf-title' %>
4
+ <div class='card-pf-body'>
5
+ <p class='card-pf-aggregate-status-notifications'>
6
+ <%= content_tag :span, nil, class: "pficon pficon-#{count > 0 ? 'error-circle-o' : 'ok'}" %>
7
+ <%= count if count > 0 %>
8
+ </p>
9
+ </div>
10
+ </div>
11
+ </div>
@@ -3,18 +3,14 @@
3
3
  <div class="list-view-pf-expand">
4
4
  <span class="fa fa-angle-right"></span>
5
5
  </div>
6
- <div class="list-view-pf-actions">
7
- <% if User.current.allowed_to?(hash_for_refresh_status_dashboard_hosts_path) %>
8
- <div class="dropdown pull-right dropdown-kebab-pf">
9
- <button class="btn btn-link dropdown-toggle" type="button" id="dropdownKebabRight<%= id %>" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
10
- <span class="fa fa-ellipsis-v"></span>
11
- </button>
12
- <ul class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownKebabRight<%= id %>">
13
- <li><%= display_link_if_authorized(_('Refresh'), hash_for_refresh_status_dashboard_hosts_path, :title => _('Refresh data'), :method => :put) %></li>
14
- </ul>
15
- </div>
16
- <% end %>
17
- </div>
6
+ <%=
7
+ render :partial => 'status_row_actions', locals: {
8
+ id: id,
9
+ supports_remediate: supports_remediate,
10
+ host_association: host_association,
11
+ owned_only: owned_only
12
+ }
13
+ %>
18
14
  <div class="list-view-pf-main-info">
19
15
  <div class="list-view-pf-left">
20
16
  <span class="pficon list-view-pf-icon-md <%= classes_for_vmware_status_row(counter) %>"></span>
@@ -0,0 +1,22 @@
1
+ <% actions = status_actions(host_association, owned_only, supports_remediate) %>
2
+
3
+ <% if actions.any? %>
4
+ <div class='list-view-pf-actions'>
5
+ <div class='dropdown pull-right dropdown-kebab-pf'>
6
+ <%= content_tag :button, class: 'btn btn-link dropdown-toggle',
7
+ type: 'button',
8
+ id: "dropdownKebabRight#{id}",
9
+ 'data-toggle': 'dropdown',
10
+ 'aria-haspopup': true,
11
+ 'aria-expanded': true do %>
12
+ <span class='fa fa-ellipsis-v' />
13
+ <% end %>
14
+
15
+ <%= content_tag :ul, class: 'dropdown-menu dropdown-menu-right', 'aria-labelledby': "dropdownKebabRight#{id}" do %>
16
+ <% actions.each do |action| %>
17
+ <%= content_tag :li, action %>
18
+ <% end %>
19
+ <% end %>
20
+ </div>
21
+ </div>
22
+ <% end %>
@@ -1,8 +1,13 @@
1
+ <%= render :partial => 'status_row_hosts_table_actions', locals: { supports_remediate: supports_remediate } %>
2
+
1
3
  <%= content_tag :table, id: status,
2
4
  class: 'table table-striped table-fixed status-hosts',
3
5
  'data-hosts-url': ajax_status_dashboard_hosts_path(status, owned_only: params[:owned_only]) do %>
4
6
  <%= content_tag :thead do %>
5
7
  <%= content_tag :tr do %>
8
+ <%= content_tag :th do %>
9
+ <%= check_box_tag 'select-all' %>
10
+ <% end %>
6
11
  <%= content_tag :th, _('Hostname') %>
7
12
  <%= content_tag :th, _('Status') %>
8
13
  <%= content_tag :th, _('Owner') %>
@@ -13,5 +18,7 @@
13
18
  <%= content_tag(:tbody) {} %>
14
19
  <% end %>
15
20
 
21
+ <%= render :partial => 'status_row_hosts_table_actions', locals: { supports_remediate: supports_remediate } %>
22
+
16
23
  <%= alert header: _("Oops, we're sorry but something went wrong"), text: '',
17
24
  class: 'alert-danger', close: false %>
@@ -0,0 +1,9 @@
1
+ <%= content_tag :div, class: 'text-right' do %>
2
+ <% if supports_remediate %>
3
+ <%= content_tag :a, _('Remediate selected'),
4
+ href: schedule_remediate_hosts_path,
5
+ class: 'btn btn-default remediate-selected',
6
+ onclick: 'show_modal(this); return false;',
7
+ disabled: true %>
8
+ <% end %>
9
+ <% end %>
@@ -1,35 +1,53 @@
1
- <% title _('Remediate %s') % @host %>
1
+ <% title _('Remediate') %>
2
2
  <% javascript 'foreman_tasks/trigger_form' %>
3
3
  <% stylesheet 'foreman_tasks/trigger_form' %>
4
4
 
5
+ <% if @statuses.any? %>
6
+ <% if @statuses.group_by(&:class).map { |i| i.first}.select { |i| i.try(:dangerous_remediate?) }.any? %>
7
+ <%= alert(:text => _('This will cause a service interruption.'), :class => 'alert-warning', :close => false) %>
8
+ <% end %>
5
9
 
6
- <% if @status.class.respond_to?(:dangerous_remediate?) && @status.class.dangerous_remediate? -%>
7
- <%= alert(:text => _('This will cause a service interruption.'), :class => 'alert-warning', :close => false) %>
8
- <% end -%>
10
+ <%= n_('One host selected for remediation.', '%s hosts selected for remediation.', @statuses.count) % @statuses.count %>
9
11
 
10
- <%= form_for @triggering, :url => submit_remediate_host_path(:id => @host.id, :status_id => @status), :html => { :class => 'form-horizontal', :id => 'schedule_remediate_form' } do |f| %>
11
- <%= javascript_tag do %>
12
- $(function() { trigger_form_selector_binds('<%= f.options[:html][:id] %>','<%= f.object_name %>') });
13
- <% end %>
14
- <div class="form-group">
15
- <label class="col-md-2 control-label"><%= _('Schedule') %></label>
16
- <div class="col-md-8">
17
- <%= fields_for :triggering, @triggering do |trigger_fields| %>
18
- <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'immediate', :text => _("Execute now") %>
19
- <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'future', :text => _("Schedule future execution") %>
12
+ <ul class='hosts-list' style='max-height: 100px; overflow-y: scroll;'>
13
+ <% @statuses.each do |status| %>
14
+ <li>
15
+ <%= status.host.name %>
16
+ <%= content_tag(:span, nil, class: 'glyphicon glyphicon-warning-sign text-warning', title: _('This will cause a service interruption.')) if status.class.try(:dangerous_remediate?) %>
17
+ </li>
18
+ <% end %>
19
+ </ul>
20
+
21
+ <%= form_for @triggering, html: { class: 'form-horizontal', id: 'schedule_remediate_form' },
22
+ url: submit_remediate_hosts_path(host_association: @statuses_params[:host_association],
23
+ owned_only: @statuses_params[:owned_only],
24
+ status_ids: @statuses_params[:status_ids]) do |f| %>
25
+ <%= javascript_tag do %>
26
+ $(function() { trigger_form_selector_binds('<%= f.options[:html][:id] %>','<%= f.object_name %>') });
27
+ <% end %>
28
+ <div class="form-group">
29
+ <label class="col-md-2 control-label"><%= _('Schedule') %></label>
30
+ <div class="col-md-8">
31
+ <%= fields_for :triggering, @triggering do |trigger_fields| %>
32
+ <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'immediate', :text => _('Execute now') %>
33
+ <%= radio_button_f trigger_fields, :mode, :class => 'trigger_mode_selector', :value => 'future', :text => _('Schedule future execution') %>
34
+ </div>
20
35
  </div>
21
- </div>
22
36
 
23
- <div class="trigger_fields">
24
- <%= content_tag(:fieldset, nil, :id => 'trigger_mode_future', :class => "trigger_mode_form #{'hidden' unless @triggering.future?}") do
25
- safe_join([
26
- text_f(f, :start_at_raw, :label => _('Start at'), :placeholder => 'YYYY-mm-dd HH:MM'),
27
- text_f(f, :start_before_raw, :label => _('Start before'), :placeholder => 'YYYY-mm-dd HH:MM',
28
- :label_help => _('Indicates that the action should be cancelled if it cannot be started before this time.'))
29
- ])
30
- end %>
31
- </div>
32
- <% end %>
37
+ <div class="trigger_fields">
38
+ <%= content_tag(:fieldset, nil, id: 'trigger_mode_future', class: "trigger_mode_form #{'hidden' unless @triggering.future?}") do
39
+ safe_join([
40
+ text_f(f, :start_at_raw, label: _('Start at'), placeholder: 'YYYY-mm-dd HH:MM'),
41
+ text_f(f, :start_before_raw, label: _('Start before'), placeholder: 'YYYY-mm-dd HH:MM',
42
+ label_help: _('Indicates that the action should be cancelled if it cannot be started before this time.'))
43
+ ])
44
+ end %>
45
+ </div>
46
+ <% end %>
33
47
 
34
- <%= submit_or_cancel f, false, :cancel_path => { :controller => :'foreman_wreckingball/hosts', :action => :status_dashboard } %>
48
+ <%= submit_or_cancel f, false, :cancel_path => { controller: :'foreman_wreckingball/hosts', action: :status_dashboard } %>
49
+ <% end %>
50
+ <% else %>
51
+ <%= content_tag :h3, _('No hosts selected') %>
52
+ <%= content_tag :p, _('Please select some hosts and try again') %>
35
53
  <% end %>
@@ -13,11 +13,11 @@
13
13
  render 'status_dashboard_empty'
14
14
  end
15
15
  %>
16
- <%= render :partial => 'common/modal', :locals => {
17
- :id => 'confirmation-modal',
18
- :title => _('Please Confirm'),
19
- :buttons => [
20
- button_tag(_('Cancel'), :class => 'btn btn-default', :data => { :dismiss => 'modal' }, :type => 'button'),
21
- button_tag(_('Submit'), :class => 'btn btn-primary', :data => { :action => 'submit' }, :onclick => 'submit_modal_form()')
16
+ <%= render partial: 'common/modal', locals: {
17
+ id: 'confirmation-modal',
18
+ title: _('Please Confirm'),
19
+ buttons: [
20
+ button_tag(_('Cancel'), class: 'btn btn-default', data: { dismiss: 'modal' }, type: 'button'),
21
+ button_tag(_('Submit'), class: 'btn btn-primary', data: { action: 'submit' }, onclick: 'submit_modal_form()')
22
22
  ]
23
23
  } %>
@@ -1,7 +1,6 @@
1
1
  <% title _('VMware Managed Hosts Overview') %>
2
- <% javascript 'foreman_wreckingball/modal' %>
3
- <% javascript 'foreman_wreckingball/status_hosts_table' %>
4
- <% stylesheet 'foreman_wreckingball/status_hosts_table' %>
2
+ <% javascript 'foreman_wreckingball/status_managed_hosts_dashboard' %>
3
+ <% stylesheet 'foreman_wreckingball/status_managed_hosts_dashboard' %>
5
4
 
6
5
  <%= title_actions(
7
6
  button_group(
@@ -13,37 +12,89 @@
13
12
  )
14
13
  ) %>
15
14
 
16
- <% if @missing_hosts.any? %>
17
- <div id="missing_vms">
18
- <h2>List of hosts not found in vSphere</h2>
19
- <div class="list-group list-view-pf list-view-pf-equalized-column" style="max-height: initial;">
20
- <% @missing_hosts.each do |host| %>
21
- <div><%= host.name %></div>
15
+ <%= render partial: 'status_managed_hosts_dashboard_cards', locals: {
16
+ missing_hosts_count: @missing_hosts.count,
17
+ duplicate_vms_count: @duplicate_vms.map(&:second).flatten.count,
18
+ different_hosts_count: @different_hosts.count
19
+ } %>
20
+
21
+ <ul class='nav nav-tabs' data-tabs='tabs'>
22
+ <li class='active'>
23
+ <%= content_tag :a, _('List of Hosts not found in vSphere'), href: '#missing_vms_tab', 'data-toggle': 'tab' %>
24
+ </li>
25
+ <li>
26
+ <%= content_tag :a, _('List of VMs with same uuid'), href: '#duplicate_vms_tab', 'data-toggle': 'tab' %>
27
+ </li>
28
+ <li>
29
+ <%= content_tag :a, _('List of VMs associated with different Compute Resources'), href: '#different_vms_tab', 'data-toggle': 'tab' %>
30
+ </li>
31
+ </ul>
32
+
33
+ <div class='tab-content'>
34
+ <div class='tab-pane active' id='missing_vms_tab'>
35
+ <% if @missing_hosts.empty? %>
36
+ <%= content_tag :p, _('No hosts to show'), class: 'ca' %>
37
+ <% else %>
38
+ <%= content_tag :table, id: 'missing_vms', class: table_css_classes do %>
39
+ <thead>
40
+ <tr>
41
+ <%= content_tag :th, _('Name') %>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ <% @missing_hosts.each do |host| %>
46
+ <tr>
47
+ <%= content_tag :td, link_to_if_authorized(host.name, hash_for_host_path(id: host)) %>
48
+ </tr>
49
+ <% end %>
50
+ </tbody>
51
+ <% end %>
22
52
  <% end %>
23
- </div>
24
53
  </div>
25
- <% end %>
26
-
27
- <% if @duplicate_vms.any? %>
28
- <div id="duplicate_vms">
29
- <h2>List of VMs with same uuid</h2>
30
- <div class="list-group list-view-pf list-view-pf-equalized-column" style="max-height: initial;">
31
- <% @duplicate_vms.each do |uuid, hosts| %>
32
- <% hosts.each do |host| %>
33
- <div><%= host.uuid %> - <%= host.name %></div>
54
+ <div class='tab-pane' id='duplicate_vms_tab'>
55
+ <% if @duplicate_vms.empty? %>
56
+ <%= content_tag :p, _('No hosts to show'), class: 'ca' %>
57
+ <% else %>
58
+ <%= content_tag :table, id: 'duplicate_vms', class: table_css_classes do %>
59
+ <thead>
60
+ <tr>
61
+ <%= content_tag :th, _('UUID') %>
62
+ <%= content_tag :th, _('Name') %>
63
+ </tr>
64
+ </thead>
65
+ <tbody>
66
+ <% @duplicate_vms.each do |_uuid, hosts| %>
67
+ <% hosts.each do |host| %>
68
+ <tr>
69
+ <%= content_tag :td, host.uuid %>
70
+ <%= content_tag :td, link_to_if_authorized(host.name, hash_for_host_path(id: host)) %>
71
+ </tr>
72
+ <% end %>
73
+ <% end %>
74
+ </tbody>
34
75
  <% end %>
35
76
  <% end %>
36
- </div>
37
77
  </div>
38
- <% end %>
39
-
40
- <% if @different_hosts.any? %>
41
- <div id="different_vms">
42
- <h2>List of VMs associated with different compute resources</h2>
43
- <div class="list-group list-view-pf list-view-pf-equalized-column" style="max-height: initial;">
44
- <% @different_hosts.each do |host| %>
45
- <div><%= host.uuid %> - <%= host.name %></div>
78
+ <div class='tab-pane' id='different_vms_tab'>
79
+ <% if @different_hosts.empty? %>
80
+ <%= content_tag :p, _('No hosts to show'), class: 'ca' %>
81
+ <% else %>
82
+ <%= content_tag :table, id: 'different_vms', class: table_css_classes do %>
83
+ <thead>
84
+ <tr>
85
+ <%= content_tag :th, _('UUID') %>
86
+ <%= content_tag :th, _('Name') %>
87
+ </tr>
88
+ </thead>
89
+ <tbody>
90
+ <% @different_hosts.each do |host| %>
91
+ <tr>
92
+ <%= content_tag :td, host.uuid %>
93
+ <%= content_tag :td, link_to_if_authorized(host.name, hash_for_host_path(id: host)) %>
94
+ </tr>
95
+ <% end %>
96
+ </tbody>
97
+ <% end %>
46
98
  <% end %>
47
- </div>
48
99
  </div>
49
- <% end %>
100
+ </div>
data/config/routes.rb CHANGED
@@ -4,15 +4,13 @@ Rails.application.routes.draw do
4
4
  scope '/wreckingball' do
5
5
  constraints(:id => /[^\/]+/) do
6
6
  resources :hosts, controller: 'foreman_wreckingball/hosts', only: [] do
7
- member do
8
- get :schedule_remediate
9
- post :submit_remediate
10
- end
11
7
  collection do
12
8
  get :status_dashboard
13
9
  get :status_managed_hosts_dashboard
14
10
  get 'status_dashboard/hosts(/:status)', as: :ajax_status_dashboard, action: :status_hosts, defaults: { format: :json }
15
11
  put :refresh_status_dashboard
12
+ get :schedule_remediate
13
+ post :submit_remediate
16
14
  end
17
15
  end
18
16
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ForemanWreckingball
4
- VERSION = '3.2.0'
4
+ VERSION = '3.3.0'
5
5
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_plugin_helper'
4
+
5
+ module Actions
6
+ module ForemanWreckingball
7
+ describe Actions::ForemanWreckingball::BulkRemediate do
8
+ include Dynflow::Testing
9
+
10
+ let(:action_class) { ::Actions::ForemanWreckingball::BulkRemediate }
11
+ let(:bulk_action) { ::Actions::BulkAction }
12
+ let(:action) { create_action(action_class) }
13
+
14
+ setup do
15
+ Setting::Wreckingball.load_defaults
16
+ FactoryBot.create_list(:host, 2, :managed, :with_wreckingball_statuses)
17
+ end
18
+
19
+ it 'plans remediate action' do
20
+ ::ForemanWreckingball::Engine::WRECKINGBALL_STATUSES.map(&:constantize)
21
+ .select(&:supports_remediate?)
22
+ .each do |status|
23
+ statuses = HostStatus::Status.where(type: status.to_s)
24
+ plan_action(action, statuses)
25
+
26
+ assert_action_planed_with(action, bulk_action, status.remediate_action, statuses.map(&:host))
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,6 +4,8 @@ require 'test_plugin_helper'
4
4
 
5
5
  module ForemanWreckingball
6
6
  class HostsControllerTest < ActionController::TestCase
7
+ include ForemanWreckingball::StatusHelper
8
+
7
9
  let(:fake_task) { OpenStruct.new(id: 123) }
8
10
 
9
11
  setup do
@@ -222,56 +224,108 @@ module ForemanWreckingball
222
224
  end
223
225
 
224
226
  describe '#schedule_remediate' do
225
- let(:host) do
226
- FactoryBot.create(:host, :with_wreckingball_statuses)
227
- end
227
+ let(:host) { FactoryBot.create(:host, :with_wreckingball_statuses) }
228
+ let(:status_ids) { [host.vmware_tools_status_object.id] }
228
229
 
229
230
  test 'shows a remediation schedule page' do
230
- get :schedule_remediate, params: { status_id: host.vmware_operatingsystem_status_object.id, id: host.id }, session: set_session_user
231
+ get :schedule_remediate, params: { status_ids: status_ids }, session: set_session_user
231
232
  assert_response :success
232
233
  end
233
234
 
234
- test 'returns not found when host id is invalid' do
235
- get :schedule_remediate, params: { status_id: nil, id: 'invalid' }, session: set_session_user
236
- assert_response :not_found
235
+ context 'with status_id' do
236
+ let(:hosts) { FactoryBot.create_list(:host, 2, :with_wreckingball_statuses) }
237
+ let(:status_ids) { [hosts.first.vmware_operatingsystem_status_object.id] }
238
+
239
+ test 'remediate selected statuses' do
240
+ get :schedule_remediate, params: { status_ids: status_ids }, session: set_session_user
241
+ assert_statuses HostStatus::Status.find(status_ids)
242
+ end
237
243
  end
238
244
 
239
- test 'returns not found when status id is invalid' do
240
- FactoryBot.create(:host, :with_wreckingball_statuses)
241
- get :schedule_remediate, params: { status_id: 'invalid', id: host.id }, session: set_session_user
242
- assert_response :not_found
245
+ context 'with host_association' do
246
+ let(:hosts) { FactoryBot.create_list(:host, 2, :with_wreckingball_statuses, owner: users(:admin)) }
247
+ let(:statuses) { hosts.map { |h| h.send(host_association) } }
248
+ let(:status_class) { ForemanWreckingball::OperatingsystemStatus }
249
+ let(:host_association) { status_class.host_association }
250
+
251
+ setup do
252
+ hosts.each { |h| h.send(host_association).update(status: status_class::MISMATCH) }
253
+ end
254
+
255
+ test 'remediate all statuses' do
256
+ get :schedule_remediate, params: { host_association: host_association }, session: set_session_user
257
+ assert_statuses statuses
258
+ end
259
+
260
+ context 'with owned_only' do
261
+ setup do
262
+ hosts_list = FactoryBot.create_list(:host, 2, :with_wreckingball_statuses, owner: users(:one))
263
+ hosts_list.each { |h| h.send(host_association).update(status: status_class::MISMATCH) }
264
+ end
265
+
266
+ test 'remediate only those statuses where the user is the owner of the host' do
267
+ get :schedule_remediate, params: { host_association: host_association, owned_only: true }, session: set_session_user
268
+ assert_statuses statuses
269
+ end
270
+ end
243
271
  end
244
272
  end
245
273
 
246
274
  describe '#submit_remediate' do
247
- let(:host) do
248
- FactoryBot.create(:host, :with_wreckingball_statuses)
275
+ let(:host) { FactoryBot.create(:host, :with_wreckingball_statuses) }
276
+ let(:status_ids) { [host.vmware_tools_status_object.id] }
277
+
278
+ setup do
279
+ ForemanTasks.stubs(:async_task).returns(fake_task)
249
280
  end
250
281
 
251
282
  test 'redirects to scheduled task' do
252
- ForemanTasks.expects(:async_task).returns(fake_task)
253
- post :submit_remediate, params: { status_id: host.vmware_operatingsystem_status_object.id, id: host.id }, session: set_session_user
283
+ post :submit_remediate, params: { status_ids: status_ids }, session: set_session_user
254
284
  assert_response :redirect
255
- assert_includes flash[:success], 'successfully scheduled'
256
- assert_redirected_to foreman_tasks_task_path(123)
285
+ assert_redirected_to foreman_tasks_task_path(fake_task.id)
257
286
  end
258
287
 
259
- test 'raises error when status can not be remediated' do
260
- FactoryBot.create(:host, :with_wreckingball_statuses)
261
- assert_raises Foreman::Exception do
262
- post :submit_remediate, params: { status_id: host.vmware_tools_status_object.id, id: host.id }, session: set_session_user
263
- end
288
+ test 'returns not found when status_ids param is invalid' do
289
+ post :submit_remediate, params: { status_ids: 'invalid' }, session: set_session_user
290
+ assert_response :not_found
264
291
  end
265
292
 
266
- test 'returns not found when host id is invalid' do
267
- post :submit_remediate, params: { status_id: nil, id: 'invalid' }, session: set_session_user
268
- assert_response :not_found
293
+ context 'with status_id' do
294
+ let(:hosts) { FactoryBot.create_list(:host, 2, :with_wreckingball_statuses) }
295
+ let(:status_ids) { [hosts.first.vmware_operatingsystem_status_object.id] }
296
+
297
+ test 'remediate selected statuses' do
298
+ post :submit_remediate, params: { status_ids: status_ids }, session: set_session_user
299
+ assert_statuses HostStatus::Status.find(status_ids)
300
+ end
269
301
  end
270
302
 
271
- test 'returns not found when status id is invalid' do
272
- FactoryBot.create(:host, :with_wreckingball_statuses)
273
- post :submit_remediate, params: { status_id: 'invalid', id: host.id }, session: set_session_user
274
- assert_response :not_found
303
+ context 'with host_association' do
304
+ let(:hosts) { FactoryBot.create_list(:host, 2, :with_wreckingball_statuses, owner: users(:admin)) }
305
+ let(:statuses) { hosts.map { |h| h.send(host_association) } }
306
+ let(:status_class) { ForemanWreckingball::OperatingsystemStatus }
307
+ let(:host_association) { status_class.host_association }
308
+
309
+ setup do
310
+ hosts.each { |h| h.send(host_association).update(status: status_class::MISMATCH) }
311
+ end
312
+
313
+ test 'remediate all statuses' do
314
+ post :submit_remediate, params: { host_association: host_association }, session: set_session_user
315
+ assert_statuses statuses
316
+ end
317
+
318
+ context 'with owned_only' do
319
+ setup do
320
+ hosts_list = FactoryBot.create_list(:host, 2, :with_wreckingball_statuses, owner: users(:one))
321
+ hosts_list.each { |h| h.send(host_association).update(status: status_class::MISMATCH) }
322
+ end
323
+
324
+ test 'remediate only those statuses where the user is the owner of the host' do
325
+ post :submit_remediate, params: { host_association: host_association, owned_only: true }, session: set_session_user
326
+ assert_statuses statuses
327
+ end
328
+ end
275
329
  end
276
330
  end
277
331
  end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanWreckingball
4
+ module StatusHelper
5
+ def assert_statuses(expected)
6
+ actual = request.env['action_controller.instance'].instance_variable_get('@statuses')
7
+ assert_equal expected, actual
8
+ end
9
+ end
10
+ end
@@ -81,7 +81,7 @@ class HostsStatusManagedHostsTest < ActionDispatch::IntegrationTest
81
81
  cr.stubs(:vms).returns([mock1_vm, mock2_vm])
82
82
  other_cr.stubs(:vms).returns([mock3_vm])
83
83
 
84
- ComputeResource.stubs(:where).returns([cr, other_cr])
84
+ Foreman::Model::Vmware.stubs(:all).returns([cr, other_cr])
85
85
 
86
86
  visit status_managed_hosts_dashboard_hosts_path
87
87
 
@@ -5,6 +5,8 @@ require 'test_helper'
5
5
  require 'database_cleaner'
6
6
  require 'dynflow/testing'
7
7
 
8
+ Dir["#{__dir__}/helpers/foreman_wreckingball/**.rb"].each { |f| require f }
9
+
8
10
  # Add plugin to FactoryBot's paths
9
11
  FactoryBot.definition_file_paths << File.join(ForemanTasks::Engine.root, 'test', 'factories')
10
12
  FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories')
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_wreckingball
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.0
4
+ version: 3.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Timo Goebel
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-01-24 00:00:00.000000000 Z
11
+ date: 2019-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: foreman-tasks
@@ -64,12 +64,16 @@ files:
64
64
  - Rakefile
65
65
  - app/assets/javascripts/foreman_wreckingball/modal.js
66
66
  - app/assets/javascripts/foreman_wreckingball/status_hosts_table.js
67
+ - app/assets/javascripts/foreman_wreckingball/status_managed_hosts_dashboard.js
67
68
  - app/assets/javascripts/foreman_wreckingball/status_row.js
68
69
  - app/assets/stylesheets/foreman_wreckingball/status_hosts_table.css.scss
70
+ - app/assets/stylesheets/foreman_wreckingball/status_managed_hosts_dashboard.css.scss
69
71
  - app/controllers/foreman_wreckingball/hosts_controller.rb
70
72
  - app/helpers/concerns/foreman_wreckingball/hosts_helper_extensions.rb
71
73
  - app/helpers/foreman_wreckingball/hypervisors_helper.rb
74
+ - app/helpers/foreman_wreckingball/statuses_helper.rb
72
75
  - app/jobs/update_hosts_vmware_facets.rb
76
+ - app/lib/actions/foreman_wreckingball/bulk_remediate.rb
73
77
  - app/lib/actions/foreman_wreckingball/host/refresh_vmware_facet.rb
74
78
  - app/lib/actions/foreman_wreckingball/host/remediate_hardware_version.rb
75
79
  - app/lib/actions/foreman_wreckingball/host/remediate_spectre_v2.rb
@@ -106,8 +110,12 @@ files:
106
110
  - app/views/foreman_wreckingball/hosts/_hosts.json.rabl
107
111
  - app/views/foreman_wreckingball/hosts/_status_dashboard_content.erb
108
112
  - app/views/foreman_wreckingball/hosts/_status_dashboard_empty.erb
113
+ - app/views/foreman_wreckingball/hosts/_status_managed_hosts_dashboard_cards.html.erb
114
+ - app/views/foreman_wreckingball/hosts/_status_managed_hosts_dashboard_cards_card.html.erb
109
115
  - app/views/foreman_wreckingball/hosts/_status_row.html.erb
116
+ - app/views/foreman_wreckingball/hosts/_status_row_actions.html.erb
110
117
  - app/views/foreman_wreckingball/hosts/_status_row_hosts_table.html.erb
118
+ - app/views/foreman_wreckingball/hosts/_status_row_hosts_table_actions.html.erb
111
119
  - app/views/foreman_wreckingball/hosts/schedule_remediate.html.erb
112
120
  - app/views/foreman_wreckingball/hosts/status_dashboard.html.erb
113
121
  - app/views/foreman_wreckingball/hosts/status_hosts.json.rabl
@@ -129,6 +137,7 @@ files:
129
137
  - locale/en/foreman_wreckingball.po
130
138
  - locale/foreman_wreckingball.pot
131
139
  - locale/gemspec.rb
140
+ - test/actions/foreman_wreckingball/bulk_remediate_test.rb
132
141
  - test/actions/foreman_wreckingball/host/refresh_vmware_facet_test.rb
133
142
  - test/actions/foreman_wreckingball/host/remediate_hardware_version_test.rb
134
143
  - test/actions/foreman_wreckingball/host/remediate_spectre_v2_test.rb
@@ -141,6 +150,7 @@ files:
141
150
  - test/factories/foreman_wreckingball_factories.rb
142
151
  - test/factories/host.rb
143
152
  - test/factories/task.rb
153
+ - test/helpers/foreman_wreckingball/status_helper.rb
144
154
  - test/integration/hosts_status_dashboard_test.rb
145
155
  - test/integration/hosts_status_managed_hosts_test.rb
146
156
  - test/integration_test_plugin_helper.rb
@@ -210,9 +220,11 @@ test_files:
210
220
  - test/actions/foreman_wreckingball/host/remediate_hardware_version_test.rb
211
221
  - test/actions/foreman_wreckingball/host/refresh_vmware_facet_test.rb
212
222
  - test/actions/foreman_wreckingball/host/remediate_spectre_v2_test.rb
223
+ - test/actions/foreman_wreckingball/bulk_remediate_test.rb
213
224
  - test/actions/foreman_wreckingball/vmware/sync_compute_resource_test.rb
214
225
  - test/actions/foreman_wreckingball/vmware/schedule_vmware_sync_test.rb
215
226
  - test/test_plugin_helper.rb
216
227
  - test/controllers/compute_resources_controller_test.rb
217
228
  - test/controllers/foreman_wreckingball/hosts_controller_test.rb
229
+ - test/helpers/foreman_wreckingball/status_helper.rb
218
230
  - test/integration_test_plugin_helper.rb