foreman_wreckingball 3.2.0 → 3.3.0

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 (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