foreman_rh_cloud 3.0.20 → 3.0.21
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.
- checksums.yaml +4 -4
- data/README.md +3 -3
- data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +17 -7
- data/app/models/insights_client_report_status.rb +58 -0
- data/app/services/foreman_rh_cloud/cloud_connector.rb +1 -1
- data/app/subscribers/foreman_rh_cloud/insights_subscriber.rb +9 -0
- data/db/seeds.d/179_ui_notifications.rb +11 -0
- data/lib/foreman_inventory_upload.rb +5 -1
- data/lib/foreman_inventory_upload/async/generate_all_reports_job.rb +8 -2
- data/lib/foreman_inventory_upload/generators/queries.rb +3 -2
- data/lib/foreman_inventory_upload/generators/tags.rb +8 -6
- data/lib/foreman_rh_cloud.rb +18 -0
- data/lib/foreman_rh_cloud/engine.rb +27 -7
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/insights_cloud/async/insights_full_sync.rb +14 -5
- data/lib/insights_cloud/async/insights_generate_notifications.rb +58 -0
- data/lib/inventory_sync/async/inventory_scheduled_sync.rb +17 -0
- data/lib/tasks/insights.rake +4 -12
- data/lib/tasks/rh_cloud_inventory.rake +12 -4
- data/package.json +1 -1
- data/test/jobs/insights_full_sync_test.rb +2 -0
- data/test/models/insights_client_report_status_test.rb +77 -0
- data/test/unit/rh_cloud_http_proxy_test.rb +4 -4
- data/test/unit/tags_generator_test.rb +10 -0
- metadata +15 -9
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: f19ab6e45bf75f915a43c8db5e829f77fe6e5e7b62998d42fe4207ce37dcc062
         | 
| 4 | 
            +
              data.tar.gz: c2bf184dcdfaefe41c1d13116ede5236c381c1f0bc8e71353d2875eee9cb289e
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 7f2e16294caeae0098b8b8367082d75af4a706f74022dba22c040d9f545b4c99ca3c31be0d3db568c2a2dfc3de5509590a9602b026caad6ea0fb1b53a3ccc9f2
         | 
| 7 | 
            +
              data.tar.gz: 9cc8d2349b5879d84bb9d2e95157c3eaafbff04823ea6704889b2678dc9a6a1f8fe300f09d28eef47c62cbbeccd94d96b0bbd7518969f8edbdbfb338a2fff1c7
         | 
    
        data/README.md
    CHANGED
    
    | @@ -42,7 +42,7 @@ In UI: Configure -> Insights -> Sync now | |
| 42 42 |  | 
| 43 43 | 
             
            From command-line:
         | 
| 44 44 |  | 
| 45 | 
            -
                /usr/sbin/foreman-rake  | 
| 45 | 
            +
                /usr/sbin/foreman-rake rh_cloud_insights:sync
         | 
| 46 46 |  | 
| 47 47 | 
             
            #### Synchronize inventory status
         | 
| 48 48 |  | 
| @@ -51,11 +51,11 @@ In UI: Configure -> Inventory Upload -> Sync inventory status | |
| 51 51 | 
             
            From command-line:
         | 
| 52 52 |  | 
| 53 53 | 
             
                # all organizations
         | 
| 54 | 
            -
                /usr/sbin/foreman-rake  | 
| 54 | 
            +
                /usr/sbin/foreman-rake rh_cloud_inventory:sync
         | 
| 55 55 |  | 
| 56 56 | 
             
                # specific organization with id 1
         | 
| 57 57 | 
             
                export organization_id=1
         | 
| 58 | 
            -
                /usr/sbin/foreman-rake  | 
| 58 | 
            +
                /usr/sbin/foreman-rake rh_cloud_inventory:sync
         | 
| 59 59 |  | 
| 60 60 | 
             
            ## TODO
         | 
| 61 61 |  | 
| @@ -10,27 +10,28 @@ module InsightsCloud::Api | |
| 10 10 |  | 
| 11 11 | 
             
                skip_after_action :log_response_body, :only => [:forward_request]
         | 
| 12 12 | 
             
                skip_before_action :check_media_type, :only => [:forward_request]
         | 
| 13 | 
            +
                after_action :update_host_insights_status, only: [:forward_request]
         | 
| 13 14 |  | 
| 14 15 | 
             
                # The method that "proxies" requests over to Cloud
         | 
| 15 16 | 
             
                def forward_request
         | 
| 16 17 | 
             
                  certs = candlepin_id_cert @organization
         | 
| 17 | 
            -
                  cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs)
         | 
| 18 | 
            +
                  @cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs)
         | 
| 18 19 |  | 
| 19 | 
            -
                  if cloud_response.code == 401
         | 
| 20 | 
            +
                  if @cloud_response.code == 401
         | 
| 20 21 | 
             
                    return render json: {
         | 
| 21 22 | 
             
                      :message => 'Authentication to the Insights Service failed.',
         | 
| 22 23 | 
             
                      :headers => {},
         | 
| 23 24 | 
             
                    }, status: :bad_gateway
         | 
| 24 25 | 
             
                  end
         | 
| 25 26 |  | 
| 26 | 
            -
                  if cloud_response.headers[:content_disposition]
         | 
| 27 | 
            -
                    return send_data cloud_response, disposition: cloud_response.headers[:content_disposition], type: cloud_response.headers[:content_type]
         | 
| 27 | 
            +
                  if @cloud_response.headers[:content_disposition]
         | 
| 28 | 
            +
                    return send_data @cloud_response, disposition: @cloud_response.headers[:content_disposition], type: @cloud_response.headers[:content_type]
         | 
| 28 29 | 
             
                  end
         | 
| 29 30 |  | 
| 30 | 
            -
                  assign_header(response, cloud_response, :x_resource_count, true)
         | 
| 31 | 
            -
                  assign_header(response, cloud_response, :x_rh_insights_request_id, false)
         | 
| 31 | 
            +
                  assign_header(response, @cloud_response, :x_resource_count, true)
         | 
| 32 | 
            +
                  assign_header(response, @cloud_response, :x_rh_insights_request_id, false)
         | 
| 32 33 |  | 
| 33 | 
            -
                  render json: cloud_response, status: cloud_response.code
         | 
| 34 | 
            +
                  render json: @cloud_response, status: @cloud_response.code
         | 
| 34 35 | 
             
                end
         | 
| 35 36 |  | 
| 36 37 | 
             
                def branch_info
         | 
| @@ -69,5 +70,14 @@ module InsightsCloud::Api | |
| 69 70 | 
             
                  @branch_id = cp_owner_id(@organization)
         | 
| 70 71 | 
             
                  return render_message "Branch ID not found for organization #{@organization.title}", :status => 400 unless @branch_id
         | 
| 71 72 | 
             
                end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                def update_host_insights_status
         | 
| 75 | 
            +
                  return unless request.path == '/redhat_access/r/insights/platform/ingress/v1/upload' ||
         | 
| 76 | 
            +
                                request.path.include?('/redhat_access/r/insights/uploads/')
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  data = @cloud_response.code.to_s.start_with?('2')
         | 
| 79 | 
            +
                  host_status = @host.get_status(InsightsClientReportStatus)
         | 
| 80 | 
            +
                  host_status.update(reported_at: Time.now.utc, status: host_status.to_status(data: data))
         | 
| 81 | 
            +
                end
         | 
| 72 82 | 
             
              end
         | 
| 73 83 | 
             
            end
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            class InsightsClientReportStatus < HostStatus::Status
         | 
| 2 | 
            +
              REPORT_INTERVAL = 48.hours
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              REPORTING             = 0 # host_registration_insights = true & getting data
         | 
| 5 | 
            +
              NO_REPORT             = 1 # host_registration_insights = true & not getting data
         | 
| 6 | 
            +
              NOT_MANAGED           = 2 # host_registration_insights = false
         | 
| 7 | 
            +
              NOT_MANAGED_WITH_DATA = 3 # host_registration_insights = false & getting data
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              def self.status_name
         | 
| 10 | 
            +
                N_('Insights')
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
              def to_label(_options = {})
         | 
| 14 | 
            +
                case status
         | 
| 15 | 
            +
                  when REPORTING
         | 
| 16 | 
            +
                    N_('Reporting')
         | 
| 17 | 
            +
                  when NO_REPORT
         | 
| 18 | 
            +
                    N_('Not reporting')
         | 
| 19 | 
            +
                  when NOT_MANAGED
         | 
| 20 | 
            +
                    N_('Not reporting (not set by registration)')
         | 
| 21 | 
            +
                  when NOT_MANAGED_WITH_DATA
         | 
| 22 | 
            +
                    N_('Reporting (not set by registration)')
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
              end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
              def to_global(_options = {})
         | 
| 27 | 
            +
                case status
         | 
| 28 | 
            +
                  when REPORTING
         | 
| 29 | 
            +
                    ::HostStatus::Global::OK
         | 
| 30 | 
            +
                  when NO_REPORT
         | 
| 31 | 
            +
                    ::HostStatus::Global::ERROR
         | 
| 32 | 
            +
                  when NOT_MANAGED
         | 
| 33 | 
            +
                    ::HostStatus::Global::OK
         | 
| 34 | 
            +
                  when NOT_MANAGED_WITH_DATA
         | 
| 35 | 
            +
                    ::HostStatus::Global::WARN
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
              end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
              def to_status(data: false)
         | 
| 40 | 
            +
                if insights_param
         | 
| 41 | 
            +
                  return REPORTING if data
         | 
| 42 | 
            +
                  return in_interval? ? REPORTING : NO_REPORT
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                data ? NOT_MANAGED_WITH_DATA : NOT_MANAGED
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              private
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              def in_interval?
         | 
| 51 | 
            +
                return false unless reported_at
         | 
| 52 | 
            +
                (Time.now.utc - reported_at).to_i < REPORT_INTERVAL.to_i
         | 
| 53 | 
            +
              end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
              def insights_param
         | 
| 56 | 
            +
                host.host_params_hash.dig('host_registration_insights', :value)
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,11 @@ | |
| 1 | 
            +
            blueprints = [
         | 
| 2 | 
            +
              {
         | 
| 3 | 
            +
                group: N_('Red Hat Insights'),
         | 
| 4 | 
            +
                name: 'insights_satellite_hits',
         | 
| 5 | 
            +
                message: N_('Satellite server has %{hits_count} recommendations by Red Hat'),
         | 
| 6 | 
            +
                level: 'warning',
         | 
| 7 | 
            +
              },
         | 
| 8 | 
            +
            ]
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            blueprints.each { |blueprint| UINotifications::Seed.new(blueprint).configure }
         | 
| 11 | 
            +
             | 
| @@ -57,10 +57,14 @@ module ForemanInventoryUpload | |
| 57 57 | 
             
              end
         | 
| 58 58 |  | 
| 59 59 | 
             
              def self.slice_size
         | 
| 60 | 
            -
                # for testing set ENV to 'https://ci.cloud.redhat.com/api/ingress/v1/upload'
         | 
| 61 60 | 
             
                @slice_size ||= (ENV['SATELLITE_INVENTORY_SLICE_SIZE'] || '1000').to_i
         | 
| 62 61 | 
             
              end
         | 
| 63 62 |  | 
| 63 | 
            +
              def self.max_org_size
         | 
| 64 | 
            +
                # Set max amount of hosts per organization for automatic uploads
         | 
| 65 | 
            +
                @max_org_size ||= (ENV['SATELLITE_INVENTORY_MAX_ORG_SIZE'] || 150_000).to_i
         | 
| 66 | 
            +
              end
         | 
| 67 | 
            +
             | 
| 64 68 | 
             
              def self.ensure_folder(folder)
         | 
| 65 69 | 
             
                FileUtils.mkdir_p(folder)
         | 
| 66 70 | 
             
                folder
         | 
| @@ -13,8 +13,14 @@ module ForemanInventoryUpload | |
| 13 13 | 
             
                    organizations = Organization.unscoped.all
         | 
| 14 14 |  | 
| 15 15 | 
             
                    organizations.map do |organization|
         | 
| 16 | 
            -
                       | 
| 17 | 
            -
             | 
| 16 | 
            +
                      total_hosts = ForemanInventoryUpload::Generators::Queries.for_org(organization.id, use_batches: false).count
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                      if total_hosts <= ForemanInventoryUpload.max_org_size
         | 
| 19 | 
            +
                        GenerateReportJob.perform_later(ForemanInventoryUpload.generated_reports_folder, organization.id)
         | 
| 20 | 
            +
                      else
         | 
| 21 | 
            +
                        logger.info("Skipping automatic uploads for organization #{organization.name}, too many hosts (#{total_hosts}/#{ForemanInventoryUpload.max_org_size})")
         | 
| 22 | 
            +
                      end
         | 
| 23 | 
            +
                    end.compact
         | 
| 18 24 | 
             
                  ensure
         | 
| 19 25 | 
             
                    self.class.set(:wait => 24.hours).perform_later
         | 
| 20 26 | 
             
                  end
         | 
| @@ -49,8 +49,9 @@ module ForemanInventoryUpload | |
| 49 49 | 
             
                    for_org(org_ids)
         | 
| 50 50 | 
             
                  end
         | 
| 51 51 |  | 
| 52 | 
            -
                  def self.for_org(organization_id)
         | 
| 53 | 
            -
                    for_slice(Host.unscoped.where(organization_id: organization_id)) | 
| 52 | 
            +
                  def self.for_org(organization_id, use_batches: true)
         | 
| 53 | 
            +
                    base_query = for_slice(Host.unscoped.where(organization_id: organization_id))
         | 
| 54 | 
            +
                    use_batches ? base_query.in_batches(of: ForemanInventoryUpload.slice_size) : base_query
         | 
| 54 55 | 
             
                  end
         | 
| 55 56 |  | 
| 56 57 | 
             
                  def self.organizations_for_user(portal_user)
         | 
| @@ -6,12 +6,14 @@ module ForemanInventoryUpload | |
| 6 6 | 
             
                  end
         | 
| 7 7 |  | 
| 8 8 | 
             
                  def generate
         | 
| 9 | 
            -
                     | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 14 | 
            -
             | 
| 9 | 
            +
                    (
         | 
| 10 | 
            +
                      locations +
         | 
| 11 | 
            +
                      hostgroups +
         | 
| 12 | 
            +
                      host_collections +
         | 
| 13 | 
            +
                      organizations +
         | 
| 14 | 
            +
                      content_data +
         | 
| 15 | 
            +
                      satellite_server_data
         | 
| 16 | 
            +
                    ).reject { |key, value| value.empty? }
         | 
| 15 17 | 
             
                  end
         | 
| 16 18 |  | 
| 17 19 | 
             
                  def generate_parameters
         | 
    
        data/lib/foreman_rh_cloud.rb
    CHANGED
    
    | @@ -38,12 +38,25 @@ module ForemanRhCloud | |
| 38 38 | 
             
              end
         | 
| 39 39 |  | 
| 40 40 | 
             
              def self.proxy_setting(logger: Foreman::Logging.logger('background'))
         | 
| 41 | 
            +
                fix_port(proxy_string(logger: logger))
         | 
| 42 | 
            +
              end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
              def self.proxy_string(logger: Foreman::Logging.logger('background'))
         | 
| 41 45 | 
             
                HttpProxy.default_global_content_proxy&.full_url ||
         | 
| 42 46 | 
             
                ForemanRhCloud.cdn_proxy(logger: logger) ||
         | 
| 43 47 | 
             
                ForemanRhCloud.global_foreman_proxy ||
         | 
| 44 48 | 
             
                ''
         | 
| 45 49 | 
             
              end
         | 
| 46 50 |  | 
| 51 | 
            +
              def self.fix_port(uri_string)
         | 
| 52 | 
            +
                return '' if uri_string.empty?
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                uri = URI(uri_string)
         | 
| 55 | 
            +
                uri.send(:define_singleton_method, :default_port, -> { nil })
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                uri.to_s
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 47 60 | 
             
              def self.cdn_proxy(logger: Foreman::Logging.logger('app'))
         | 
| 48 61 | 
             
                proxy_config = SETTINGS[:katello][:cdn_proxy]
         | 
| 49 62 | 
             
                return nil unless proxy_config
         | 
| @@ -82,4 +95,9 @@ module ForemanRhCloud | |
| 82 95 |  | 
| 83 96 | 
             
                transformed_uri.to_s
         | 
| 84 97 | 
             
              end
         | 
| 98 | 
            +
             | 
| 99 | 
            +
              # For testing purposes we can override the default hostname with an environment variable SATELLITE_RH_CLOUD_FOREMAN_HOST
         | 
| 100 | 
            +
              def self.foreman_host
         | 
| 101 | 
            +
                @foreman_host ||= ::Host.unscoped.friendly.find(ENV['SATELLITE_RH_CLOUD_FOREMAN_HOST'] || ::SmartProxy.default_capsule.name)
         | 
| 102 | 
            +
              end
         | 
| 85 103 | 
             
            end
         | 
| @@ -78,7 +78,10 @@ module ForemanRhCloud | |
| 78 78 |  | 
| 79 79 | 
             
                    register_global_js_file 'global'
         | 
| 80 80 |  | 
| 81 | 
            -
                    register_custom_status | 
| 81 | 
            +
                    register_custom_status InventorySync::InventoryStatus
         | 
| 82 | 
            +
                    register_custom_status InsightsClientReportStatus
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                    subscribe 'host_created.event.foreman', ForemanRhCloud::InsightsSubscriber
         | 
| 82 85 |  | 
| 83 86 | 
             
                    extend_page 'hosts/show' do |context|
         | 
| 84 87 | 
             
                      context.add_pagelet :main_tabs,
         | 
| @@ -119,12 +122,29 @@ module ForemanRhCloud | |
| 119 122 | 
             
                end
         | 
| 120 123 |  | 
| 121 124 | 
             
                config.to_prepare do
         | 
| 122 | 
            -
                   | 
| 123 | 
            -
             | 
| 124 | 
            -
             | 
| 125 | 
            -
                     | 
| 126 | 
            -
             | 
| 127 | 
            -
             | 
| 125 | 
            +
                  # skip database manipulations while tables do not exist, like in migrations
         | 
| 126 | 
            +
                  if ActiveRecord::Base.connection.data_source_exists?(ForemanTasks::Task.table_name) &&
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                    RemoteExecutionFeature.register(
         | 
| 129 | 
            +
                      :rh_cloud_remediate_hosts,
         | 
| 130 | 
            +
                      N_('Apply Insights recommendations'),
         | 
| 131 | 
            +
                      description: N_('Run remediation playbook generated by Insights'),
         | 
| 132 | 
            +
                      host_action_button: false
         | 
| 133 | 
            +
                    )
         | 
| 134 | 
            +
                    # skip object creation when admin user is not present, for example in test DB
         | 
| 135 | 
            +
                    if User.unscoped.find_by_login(User::ANONYMOUS_ADMIN).present?
         | 
| 136 | 
            +
                      unless ForemanTasks::RecurringLogic.joins(:tasks).merge(
         | 
| 137 | 
            +
                        ForemanTasks::Task.where(label: 'InventorySync::Async::InventoryScheduledSync')
         | 
| 138 | 
            +
                      ).exists?
         | 
| 139 | 
            +
                        User.as_anonymous_admin do
         | 
| 140 | 
            +
                          recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline("0 0 * * *")
         | 
| 141 | 
            +
                          recurring_logic.save!
         | 
| 142 | 
            +
                          recurring_logic.start(InventorySync::Async::InventoryScheduledSync)
         | 
| 143 | 
            +
                        end
         | 
| 144 | 
            +
                      end
         | 
| 145 | 
            +
                    end
         | 
| 146 | 
            +
                  end
         | 
| 147 | 
            +
                rescue ActiveRecord::NoDatabaseError
         | 
| 128 148 | 
             
                end
         | 
| 129 149 | 
             
              end
         | 
| 130 150 | 
             
            end
         | 
| @@ -6,11 +6,16 @@ module InsightsCloud | |
| 6 6 | 
             
                  include ::ForemanRhCloud::CloudAuth
         | 
| 7 7 |  | 
| 8 8 | 
             
                  def plan
         | 
| 9 | 
            -
                     | 
| 10 | 
            -
             | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 13 | 
            -
             | 
| 9 | 
            +
                    sequence do
         | 
| 10 | 
            +
                      # This can be turned off when we enable automatic status syncs
         | 
| 11 | 
            +
                      # This step will query cloud inventory to retrieve inventory uuids for each host
         | 
| 12 | 
            +
                      plan_hosts_sync
         | 
| 13 | 
            +
                      plan_self
         | 
| 14 | 
            +
                      concurrence do
         | 
| 15 | 
            +
                        plan_rules_sync
         | 
| 16 | 
            +
                        plan_notifications
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                    end
         | 
| 14 19 | 
             
                  end
         | 
| 15 20 |  | 
| 16 21 | 
             
                  def run
         | 
| @@ -40,6 +45,10 @@ module InsightsCloud | |
| 40 45 | 
             
                    plan_action InsightsRulesSync
         | 
| 41 46 | 
             
                  end
         | 
| 42 47 |  | 
| 48 | 
            +
                  def plan_notifications
         | 
| 49 | 
            +
                    plan_action InsightsGenerateNotifications
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 43 52 | 
             
                  def query_insights_hits
         | 
| 44 53 | 
             
                    hits_response = RestClient::Request.execute(
         | 
| 45 54 | 
             
                      method: :get,
         | 
| @@ -0,0 +1,58 @@ | |
| 1 | 
            +
            require 'rest-client'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module InsightsCloud
         | 
| 4 | 
            +
              module Async
         | 
| 5 | 
            +
                class InsightsGenerateNotifications < ::Actions::EntryAction
         | 
| 6 | 
            +
                  # cache blueprint on class level, so it won't be reloaded on subsequent calls
         | 
| 7 | 
            +
                  def self.blueprint
         | 
| 8 | 
            +
                    @blueprint ||= NotificationBlueprint.find_by(name: 'insights_satellite_hits')
         | 
| 9 | 
            +
                  end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  def run
         | 
| 12 | 
            +
                    add_satellite_notifications
         | 
| 13 | 
            +
                  end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def add_satellite_notifications
         | 
| 16 | 
            +
                    hits_count = InsightsHit.where(host_id: foreman_host.id).count
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # Remove stale notifications
         | 
| 19 | 
            +
                    blueprint.notifications.destroy_all
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    if hits_count > 0
         | 
| 22 | 
            +
                      add_notification(hits_count)
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def logger
         | 
| 29 | 
            +
                    action_logger
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                  def foreman_host
         | 
| 33 | 
            +
                    ForemanRhCloud.foreman_host
         | 
| 34 | 
            +
                  end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  def blueprint
         | 
| 37 | 
            +
                    self.class.blueprint
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  def add_notification(hits_count)
         | 
| 41 | 
            +
                    Notification.create!(
         | 
| 42 | 
            +
                      initiator: User.anonymous_admin,
         | 
| 43 | 
            +
                      audience: ::Notification::AUDIENCE_ADMIN,
         | 
| 44 | 
            +
                      message: UINotifications::StringParser.new(blueprint.message, {hits_count: hits_count}).to_s,
         | 
| 45 | 
            +
                      notification_blueprint: blueprint,
         | 
| 46 | 
            +
                      actions: {
         | 
| 47 | 
            +
                        links: [
         | 
| 48 | 
            +
                          {
         | 
| 49 | 
            +
                            href: Rails.application.routes.url_helpers.foreman_rh_cloud_insights_cloud_path(search: "hostname=#{foreman_host.name}"),
         | 
| 50 | 
            +
                            title: _('Fix host'),
         | 
| 51 | 
            +
                          },
         | 
| 52 | 
            +
                        ],
         | 
| 53 | 
            +
                      }
         | 
| 54 | 
            +
                    )
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module InventorySync
         | 
| 2 | 
            +
              module Async
         | 
| 3 | 
            +
                class InventoryScheduledSync < ::Actions::EntryAction
         | 
| 4 | 
            +
                  include ::Actions::RecurringAction
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  def plan
         | 
| 7 | 
            +
                    Organization.unscoped.each do |org|
         | 
| 8 | 
            +
                      plan_org_sync(org)
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                  def plan_org_sync(org)
         | 
| 13 | 
            +
                    plan_action InventoryFullSync, org
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        data/lib/tasks/insights.rake
    CHANGED
    
    | @@ -1,15 +1,7 @@ | |
| 1 1 | 
             
            namespace :rh_cloud_insights do
         | 
| 2 | 
            -
              desc "Synchronize Insights  | 
| 3 | 
            -
              task sync: :environment do
         | 
| 4 | 
            -
                 | 
| 5 | 
            -
             | 
| 6 | 
            -
                else
         | 
| 7 | 
            -
                  organizations = Organization.all
         | 
| 8 | 
            -
                end
         | 
| 9 | 
            -
             | 
| 10 | 
            -
                organizations.each do |organization|
         | 
| 11 | 
            -
                  ForemanTasks.async_task(InventorySync::Async::InventoryFullSync, organization)
         | 
| 12 | 
            -
                  puts "Synchronized inventory for organization '#{organization.name}'"
         | 
| 13 | 
            -
                end
         | 
| 2 | 
            +
              desc "Synchronize Insights hosts hits"
         | 
| 3 | 
            +
              task sync: [:environment, 'dynflow:client'] do
         | 
| 4 | 
            +
                ForemanTasks.sync_task(InsightsCloud::Async::InsightsFullSync)
         | 
| 5 | 
            +
                puts "Synchronized Insights hosts hits data"
         | 
| 14 6 | 
             
              end
         | 
| 15 7 | 
             
            end
         | 
| @@ -52,9 +52,17 @@ namespace :rh_cloud_inventory do | |
| 52 52 | 
             
                end
         | 
| 53 53 | 
             
              end
         | 
| 54 54 |  | 
| 55 | 
            -
              desc "Synchronize  | 
| 56 | 
            -
              task sync: :environment do
         | 
| 57 | 
            -
                 | 
| 58 | 
            -
             | 
| 55 | 
            +
              desc "Synchronize Hosts inventory"
         | 
| 56 | 
            +
              task sync: [:environment, 'dynflow:client'] do
         | 
| 57 | 
            +
                if ! ENV['organization_id'].nil?
         | 
| 58 | 
            +
                  organizations = [ Organization.where(:id => ENV['organization_id']).first ]
         | 
| 59 | 
            +
                else
         | 
| 60 | 
            +
                  organizations = Organization.all
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                organizations.each do |organization|
         | 
| 64 | 
            +
                  ForemanTasks.async_task(InventorySync::Async::InventoryFullSync, organization)
         | 
| 65 | 
            +
                  puts "Synchronized inventory for organization '#{organization.name}'"
         | 
| 66 | 
            +
                end
         | 
| 59 67 | 
             
              end
         | 
| 60 68 | 
             
            end
         | 
    
        data/package.json
    CHANGED
    
    
| @@ -3,6 +3,7 @@ require 'test_helper' | |
| 3 3 | 
             
            class InsightsFullSyncTest < ActiveSupport::TestCase
         | 
| 4 4 | 
             
              setup do
         | 
| 5 5 | 
             
                InsightsCloud::Async::InsightsFullSync.any_instance.stubs(:plan_rules_sync)
         | 
| 6 | 
            +
                InsightsCloud::Async::InsightsFullSync.any_instance.stubs(:plan_notifications)
         | 
| 6 7 |  | 
| 7 8 | 
             
                uuid1 = 'accdf444-5628-451d-bf3e-cf909ad72756'
         | 
| 8 9 | 
             
                @host1 = FactoryBot.create(:host, :managed, name: 'host1')
         | 
| @@ -60,6 +61,7 @@ class InsightsFullSyncTest < ActiveSupport::TestCase | |
| 60 61 |  | 
| 61 62 | 
             
                InsightsCloud::Async::InsightsFullSync.any_instance.expects(:plan_hosts_sync)
         | 
| 62 63 | 
             
                InsightsCloud::Async::InsightsFullSync.any_instance.expects(:plan_rules_sync)
         | 
| 64 | 
            +
                InsightsCloud::Async::InsightsFullSync.any_instance.expects(:plan_notifications)
         | 
| 63 65 | 
             
                ForemanTasks.sync_task(InsightsCloud::Async::InsightsFullSync)
         | 
| 64 66 |  | 
| 65 67 | 
             
                @host1.reload
         | 
| @@ -0,0 +1,77 @@ | |
| 1 | 
            +
            require 'test_plugin_helper'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class InsightsClientReportStatusTest < ActiveSupport::TestCase
         | 
| 4 | 
            +
              describe 'to_status' do
         | 
| 5 | 
            +
                let(:host) { FactoryBot.create(:host, :managed) }
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                setup do
         | 
| 8 | 
            +
                  CommonParameter.where(name: 'host_registration_insights').destroy_all
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                test 'host_registration_insights = true & is getting data' do
         | 
| 12 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
         | 
| 13 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  assert_equal 0, host_status.to_status(data: true)
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                test 'host_registration_insights = true & no data in less than 48 hours' do
         | 
| 19 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
         | 
| 20 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 21 | 
            +
                  host_status.update(reported_at: 1.day.ago)
         | 
| 22 | 
            +
                  assert_equal 0, host_status.to_status
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                test 'host_registration_insights = true & no data in more than 48 hours' do
         | 
| 26 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
         | 
| 27 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 28 | 
            +
                  host_status.update(reported_at: 3.days.ago)
         | 
| 29 | 
            +
                  assert_equal 1, host_status.to_status
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                test 'host_registration_insights = false & no data' do
         | 
| 33 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false)
         | 
| 34 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 35 | 
            +
                  assert_equal 2, host_status.to_status
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                test 'host_registration_insights = false & getting data' do
         | 
| 39 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false)
         | 
| 40 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 41 | 
            +
                  assert_equal 3, host_status.to_status(data: true)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                test 'host_registration_insights = nil & is getting data' do
         | 
| 45 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 46 | 
            +
                  assert_equal 3, host_status.to_status(data: true)
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                test 'host_registration_insights = nil & no data in less than 48 hours' do
         | 
| 50 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 51 | 
            +
                  host_status.update(reported_at: 1.day.ago)
         | 
| 52 | 
            +
                  assert_equal 2, host_status.to_status
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
             | 
| 55 | 
            +
                test 'host_registration_insights = nil & no data in more than 48 hours' do
         | 
| 56 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 57 | 
            +
                  host_status.update(reported_at: 3.days.ago)
         | 
| 58 | 
            +
                  assert_equal 2, host_status.to_status
         | 
| 59 | 
            +
                end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                test 'override param on host level from `false` to `true`' do
         | 
| 62 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false)
         | 
| 63 | 
            +
                  FactoryBot.create(:host_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true, host: host)
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 66 | 
            +
                  assert_equal 0, host_status.to_status(data: true)
         | 
| 67 | 
            +
                end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                test 'override param on host level from `true` to `false`' do
         | 
| 70 | 
            +
                  FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
         | 
| 71 | 
            +
                  FactoryBot.create(:host_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false, host: host)
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                  host_status = Host.find_by_name(host.name).reload.get_status(InsightsClientReportStatus)
         | 
| 74 | 
            +
                  assert_equal 2, host_status.to_status
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
              end
         | 
| 77 | 
            +
            end
         | 
| @@ -2,16 +2,16 @@ require 'test_plugin_helper' | |
| 2 2 |  | 
| 3 3 | 
             
            class RhCloudHttpProxyTest < ActiveSupport::TestCase
         | 
| 4 4 | 
             
              setup do
         | 
| 5 | 
            -
                @global_content_proxy_mock = 'http://global:content@localhost: | 
| 6 | 
            -
                @global_foreman_proxy_mock = 'http://global:foreman@localhost: | 
| 5 | 
            +
                @global_content_proxy_mock = 'http://global:content@localhost:80'
         | 
| 6 | 
            +
                @global_foreman_proxy_mock = 'http://global:foreman@localhost:80'
         | 
| 7 7 | 
             
                @katello_cdn_proxy_mock = {
         | 
| 8 8 | 
             
                  host: 'localhost',
         | 
| 9 | 
            -
                  port: ' | 
| 9 | 
            +
                  port: '80',
         | 
| 10 10 | 
             
                  user: 'katello',
         | 
| 11 11 | 
             
                  password: 'cdn',
         | 
| 12 12 | 
             
                  scheme: 'http',
         | 
| 13 13 | 
             
                }
         | 
| 14 | 
            -
                @katello_cdn_proxy_string_mock = 'http://katello:cdn@localhost: | 
| 14 | 
            +
                @katello_cdn_proxy_string_mock = 'http://katello:cdn@localhost:80'
         | 
| 15 15 | 
             
              end
         | 
| 16 16 |  | 
| 17 17 | 
             
              test 'selects global content proxy' do
         | 
| @@ -52,6 +52,16 @@ class TagsGeneratorTest < ActiveSupport::TestCase | |
| 52 52 | 
             
                assert_equal @host.organization_id.to_s, actual['organization_id'].first.last
         | 
| 53 53 | 
             
              end
         | 
| 54 54 |  | 
| 55 | 
            +
              test 'filters tags with empty values' do
         | 
| 56 | 
            +
                generator = create_generator
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                @host.stubs(:content_view)
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                actual = generator.generate.group_by { |key, value| key }
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                assert_equal false, actual.key?('content_view')
         | 
| 63 | 
            +
              end
         | 
| 64 | 
            +
             | 
| 55 65 | 
             
              private
         | 
| 56 66 |  | 
| 57 67 | 
             
              def create_generator
         | 
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: foreman_rh_cloud
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3.0. | 
| 4 | 
            +
              version: 3.0.21
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Foreman Red Hat Cloud team
         | 
| 8 | 
            -
            autorequire: | 
| 8 | 
            +
            autorequire:
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date: 2021- | 
| 11 | 
            +
            date: 2021-05-20 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: katello
         | 
| @@ -160,6 +160,7 @@ files: | |
| 160 160 | 
             
            - app/helpers/foreman_inventory_upload_helper.rb
         | 
| 161 161 | 
             
            - app/helpers/foreman_inventory_upload_host_helper.rb
         | 
| 162 162 | 
             
            - app/models/concerns/rh_cloud_host.rb
         | 
| 163 | 
            +
            - app/models/insights_client_report_status.rb
         | 
| 163 164 | 
             
            - app/models/insights_facet.rb
         | 
| 164 165 | 
             
            - app/models/insights_hit.rb
         | 
| 165 166 | 
             
            - app/models/insights_resolution.rb
         | 
| @@ -174,6 +175,7 @@ files: | |
| 174 175 | 
             
            - app/services/foreman_rh_cloud/cloud_request_forwarder.rb
         | 
| 175 176 | 
             
            - app/services/foreman_rh_cloud/remediations_retriever.rb
         | 
| 176 177 | 
             
            - app/services/foreman_rh_cloud/template_renderer_helper.rb
         | 
| 178 | 
            +
            - app/subscribers/foreman_rh_cloud/insights_subscriber.rb
         | 
| 177 179 | 
             
            - app/views/hosts/_insights_tab.html.erb
         | 
| 178 180 | 
             
            - app/views/job_templates/rh_cloud_remediations.erb
         | 
| 179 181 | 
             
            - app/views/layouts/foreman_rh_cloud/application.html.erb
         | 
| @@ -190,6 +192,7 @@ files: | |
| 190 192 | 
             
            - db/migrate/20210214000002_add_rule_id_to_hits.foreman_rh_cloud.rb
         | 
| 191 193 | 
             
            - db/migrate/20210307000001_add_unique_to_insights_facet.foreman_rh_cloud.rb
         | 
| 192 194 | 
             
            - db/migrate/20210404000001_change_resolutions.foreman_rh_cloud.rb
         | 
| 195 | 
            +
            - db/seeds.d/179_ui_notifications.rb
         | 
| 193 196 | 
             
            - db/seeds.d/50_job_templates.rb
         | 
| 194 197 | 
             
            - lib/foreman_inventory_upload.rb
         | 
| 195 198 | 
             
            - lib/foreman_inventory_upload/async/async_helpers.rb
         | 
| @@ -213,6 +216,7 @@ files: | |
| 213 216 | 
             
            - lib/foreman_rh_cloud/version.rb
         | 
| 214 217 | 
             
            - lib/insights_cloud.rb
         | 
| 215 218 | 
             
            - lib/insights_cloud/async/insights_full_sync.rb
         | 
| 219 | 
            +
            - lib/insights_cloud/async/insights_generate_notifications.rb
         | 
| 216 220 | 
             
            - lib/insights_cloud/async/insights_resolutions_sync.rb
         | 
| 217 221 | 
             
            - lib/insights_cloud/async/insights_rules_sync.rb
         | 
| 218 222 | 
             
            - lib/insights_cloud/async/insights_scheduled_sync.rb
         | 
| @@ -220,6 +224,7 @@ files: | |
| 220 224 | 
             
            - lib/inventory_sync/async/host_result.rb
         | 
| 221 225 | 
             
            - lib/inventory_sync/async/inventory_full_sync.rb
         | 
| 222 226 | 
             
            - lib/inventory_sync/async/inventory_hosts_sync.rb
         | 
| 227 | 
            +
            - lib/inventory_sync/async/inventory_scheduled_sync.rb
         | 
| 223 228 | 
             
            - lib/inventory_sync/async/query_inventory_job.rb
         | 
| 224 229 | 
             
            - lib/tasks/foreman_rh_cloud_tasks.rake
         | 
| 225 230 | 
             
            - lib/tasks/insights.rake
         | 
| @@ -242,6 +247,7 @@ files: | |
| 242 247 | 
             
            - test/jobs/insights_rules_sync_test.rb
         | 
| 243 248 | 
             
            - test/jobs/inventory_full_sync_test.rb
         | 
| 244 249 | 
             
            - test/jobs/upload_report_job_test.rb
         | 
| 250 | 
            +
            - test/models/insights_client_report_status_test.rb
         | 
| 245 251 | 
             
            - test/test_plugin_helper.rb
         | 
| 246 252 | 
             
            - test/unit/archived_report_generator_test.rb
         | 
| 247 253 | 
             
            - test/unit/fact_helpers_test.rb
         | 
| @@ -630,7 +636,7 @@ homepage: https://github.com/theforeman/foreman_rh_cloud | |
| 630 636 | 
             
            licenses:
         | 
| 631 637 | 
             
            - GPL-3.0
         | 
| 632 638 | 
             
            metadata: {}
         | 
| 633 | 
            -
            post_install_message: | 
| 639 | 
            +
            post_install_message:
         | 
| 634 640 | 
             
            rdoc_options: []
         | 
| 635 641 | 
             
            require_paths:
         | 
| 636 642 | 
             
            - lib
         | 
| @@ -645,9 +651,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 645 651 | 
             
                - !ruby/object:Gem::Version
         | 
| 646 652 | 
             
                  version: '0'
         | 
| 647 653 | 
             
            requirements: []
         | 
| 648 | 
            -
             | 
| 649 | 
            -
             | 
| 650 | 
            -
            signing_key: 
         | 
| 654 | 
            +
            rubygems_version: 3.2.15
         | 
| 655 | 
            +
            signing_key:
         | 
| 651 656 | 
             
            specification_version: 4
         | 
| 652 657 | 
             
            summary: Summary of ForemanRhCloud.
         | 
| 653 658 | 
             
            test_files:
         | 
| @@ -657,13 +662,14 @@ test_files: | |
| 657 662 | 
             
            - test/controllers/reports_controller_test.rb
         | 
| 658 663 | 
             
            - test/controllers/uploads_controller_test.rb
         | 
| 659 664 | 
             
            - test/controllers/uploads_settings_controller_test.rb
         | 
| 660 | 
            -
            - test/factories/inventory_upload_factories.rb
         | 
| 661 665 | 
             
            - test/factories/insights_factories.rb
         | 
| 662 | 
            -
            - test/ | 
| 666 | 
            +
            - test/factories/inventory_upload_factories.rb
         | 
| 663 667 | 
             
            - test/jobs/insights_full_sync_test.rb
         | 
| 664 668 | 
             
            - test/jobs/insights_resolutions_sync_test.rb
         | 
| 665 669 | 
             
            - test/jobs/insights_rules_sync_test.rb
         | 
| 666 670 | 
             
            - test/jobs/inventory_full_sync_test.rb
         | 
| 671 | 
            +
            - test/jobs/upload_report_job_test.rb
         | 
| 672 | 
            +
            - test/models/insights_client_report_status_test.rb
         | 
| 667 673 | 
             
            - test/test_plugin_helper.rb
         | 
| 668 674 | 
             
            - test/unit/archived_report_generator_test.rb
         | 
| 669 675 | 
             
            - test/unit/fact_helpers_test.rb
         |