diff --git a/lib/task_helpers/exports/reports.rb b/lib/task_helpers/exports/reports.rb new file mode 100644 index 00000000000..6974d4de8d0 --- /dev/null +++ b/lib/task_helpers/exports/reports.rb @@ -0,0 +1,16 @@ +module TaskHelpers + class Exports + class Reports + def export(options = {}) + export_dir = options[:directory] + + custom_reports = options[:all] ? MiqReport.all : MiqReport.where(:rpt_type => "Custom") + + custom_reports.each do |report| + filename = Exports.safe_filename(report.name, options[:keep_spaces]) + File.write("#{export_dir}/#{filename}.yaml", report.export_to_array.to_yaml) + end + end + end + end +end diff --git a/lib/task_helpers/imports.rb b/lib/task_helpers/imports.rb index e823e4a6d74..1cb5c1183c2 100644 --- a/lib/task_helpers/imports.rb +++ b/lib/task_helpers/imports.rb @@ -4,6 +4,7 @@ def self.parse_options require 'trollop' options = Trollop.options(EvmRakeHelper.extract_command_options) do opt :source, 'Directory or file to import from', :type => :string, :required => true + opt :overwrite, 'Overwrite existing object', :type => :boolean, :default => true end error = validate_source(options[:source]) diff --git a/lib/task_helpers/imports/reports.rb b/lib/task_helpers/imports/reports.rb new file mode 100644 index 00000000000..1f369865c1f --- /dev/null +++ b/lib/task_helpers/imports/reports.rb @@ -0,0 +1,26 @@ +module TaskHelpers + class Imports + class Reports + def import(options = {}) + return unless options[:source] + + glob = File.file?(options[:source]) ? options[:source] : "#{options[:source]}/*.yaml" + Dir.glob(glob) do |filename| + $log.info("Importing Reports from: #{filename}") + + report_options = { :userid => 'admin', + :overwrite => options[:overwrite], + :save => true } + + begin + report_fd = File.open(filename, 'r') + MiqReport.import(report_fd, report_options) + rescue ActiveModel::UnknownAttributeError, RuntimeError => err + $log.error("Error importing #{filename} : #{err.message}") + warn("Error importing #{filename} : #{err.message}") + end + end + end + end + end +end diff --git a/lib/tasks/evm_export_import.rake b/lib/tasks/evm_export_import.rake index 4edbeef672f..7cb64ec20b7 100644 --- a/lib/tasks/evm_export_import.rake +++ b/lib/tasks/evm_export_import.rake @@ -8,6 +8,7 @@ # * Custom Buttons # * SmartState Analysis Scan Profiles # * Customization Templates +# * Reports namespace :evm do namespace :export do @@ -96,6 +97,14 @@ namespace :evm do exit # exit so that parameters to the first rake task are not run as rake tasks end + + desc 'Exports all reports to individual YAML files' + task :reports => :environment do + options = TaskHelpers::Exports.parse_options + TaskHelpers::Exports::Reports.new.export(options) + + exit # exit so that parameters to the first rake task are not run as rake tasks + end end namespace :import do @@ -184,5 +193,13 @@ namespace :evm do exit # exit so that parameters to the first rake task are not run as rake tasks end + + desc 'Imports all reports from individual YAML files' + task :reports => :environment do + options = TaskHelpers::Imports.parse_options + TaskHelpers::Imports::Reports.new.import(options) + + exit # exit so that parameters to the first rake task are not run as rake tasks + end end end diff --git a/spec/lib/task_helpers/exports/reports_spec.rb b/spec/lib/task_helpers/exports/reports_spec.rb new file mode 100644 index 00000000000..bd3276ff59d --- /dev/null +++ b/spec/lib/task_helpers/exports/reports_spec.rb @@ -0,0 +1,67 @@ +describe TaskHelpers::Exports::Reports do + let(:export_dir) do + Dir.mktmpdir('miq_exp_dir') + end + + before do + FactoryBot.create(:miq_report, + :name => "Test Report", + :rpt_type => "Custom", + :tz => "Eastern Time (US & Canada)", + :col_order => %w(name boot_time disks_aligned), + :cols => %w(name boot_time disks_aligned), + :db_options => { :rpt_type => "ChargebackContainerProject" }, + "include" => { "columns" => %w(col1 col2) }) + FactoryBot.create(:miq_report, + :name => "Test Report 2", + :rpt_type => "Custom", + :tz => "Eastern Time (US & Canada)", + :col_order => %w(name boot_time disks_aligned), + :cols => %w(name boot_time disks_aligned), + :db_options => { :rpt_type => "ChargebackContainerProject" }, + "include" => { "columns" => %w(col1 col2) }) + FactoryBot.create(:miq_report, + :name => "Default Test Report", + :rpt_type => "Default", + :tz => "Eastern Time (US & Canada)", + :col_order => %w(name boot_time disks_aligned), + :cols => %w(name boot_time disks_aligned), + :db_options => { :rpt_type => "ChargebackContainerProject" }, + "include" => { "columns" => %w(col1 col2) }) + end + + after do + FileUtils.remove_entry export_dir + end + + describe "when --all is not specified" do + let(:report_filename1) { "#{export_dir}/Test_Report.yaml" } + let(:report_filename2) { "#{export_dir}/Test_Report_2.yaml" } + + it "exports custom reports to individual files in a given directory" do + TaskHelpers::Exports::Reports.new.export(:directory => export_dir) + expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(2) + report1 = YAML.load_file(report_filename1) + expect(report1.first["MiqReport"]["menu_name"]).to eq("Test Report") + report2 = YAML.load_file(report_filename2) + expect(report2.first["MiqReport"]["menu_name"]).to eq("Test Report 2") + end + end + + describe "when --all is specified" do + let(:report_filename1) { "#{export_dir}/Test_Report.yaml" } + let(:report_filename2) { "#{export_dir}/Test_Report_2.yaml" } + let(:report_filename3) { "#{export_dir}/Default_Test_Report.yaml" } + + it "exports all reports to individual files in a given directory" do + TaskHelpers::Exports::Reports.new.export(:directory => export_dir, :all => true) + expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(3) + report1 = YAML.load_file(report_filename1) + expect(report1.first["MiqReport"]["menu_name"]).to eq("Test Report") + report2 = YAML.load_file(report_filename2) + expect(report2.first["MiqReport"]["menu_name"]).to eq("Test Report 2") + report3 = YAML.load_file(report_filename3) + expect(report3.first["MiqReport"]["menu_name"]).to eq("Default Test Report") + end + end +end diff --git a/spec/lib/task_helpers/imports/data/reports/Test_Report.yaml b/spec/lib/task_helpers/imports/data/reports/Test_Report.yaml new file mode 100644 index 00000000000..38db49afbf5 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/reports/Test_Report.yaml @@ -0,0 +1,86 @@ +--- +- MiqReport: + title: Test Report for Exporting + rpt_group: Custom + rpt_type: Custom + priority: + db: Vm + cols: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + include: {} + col_order: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + headers: + - Name + - IP Addresses + - OS Name + - Number of CPUs + - Cpu Cores Per Socket + - Number of CPU Cores + - Memory + - Evm Owner Name + - Evm Owner Email + conditions: !ruby/object:MiqExpression + exp: + CONTAINS: + tag: Vm.managed-department + value: engineering + context_type: + order: Ascending + sortby: + - evm_owner_email + - name + group: y + graph: + dims: + filename: + file_mtime: + categories: [] + timeline: + template_type: report + where_clause: + db_options: {} + generate_cols: + generate_rows: + col_formats: + - + - + - + - + - + - + - + - + - + tz: + time_profile_id: + display_filter: + col_options: + evm_owner_email: + :break_label: 'Evm Owner Email: ' + :break_format: :model_name + rpt_options: + :pdf: + :page_size: US Letter - 8.5in x 11.0in + :queue_timeout: + :summary: + :hide_detail_rows: false + miq_group_id: 2 + user_id: 1 + menu_name: Test Report diff --git a/spec/lib/task_helpers/imports/data/reports/Test_Report_-_Chargeback.yaml b/spec/lib/task_helpers/imports/data/reports/Test_Report_-_Chargeback.yaml new file mode 100644 index 00000000000..0dd9eb460c9 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/reports/Test_Report_-_Chargeback.yaml @@ -0,0 +1,157 @@ +--- +- MiqReport: + title: Test Chargeback Report + rpt_group: Custom + rpt_type: Custom + priority: + db: ChargebackVm + cols: + - start_date + - display_range + - vm_name + - cpu_used_cost + - memory_used_cost + - storage_allocated_cost + - total_cost + include: {} + col_order: + - vm_name + - display_range + - cpu_used_cost + - memory_used_cost + - storage_allocated_cost + - total_cost + headers: + - VM Name + - Date Range + - CPU Used Cost + - Memory Used Cost + - Storage Allocated Cost + - Total Cost + conditions: + order: Ascending + sortby: + - vm_name + - start_date + group: y + graph: + dims: + filename: + file_mtime: + categories: [] + timeline: + template_type: report + where_clause: + db_options: + :rpt_type: ChargebackVm + :options: + :interval: daily + :interval_size: 1 + :end_interval_offset: 1 + :tenant_id: '1' + :method_for_allocated_metrics: :max + :include_metrics: true + :cumulative_rate_calculation: false + :groupby: date + :groupby_tag: + :groupby_label: + generate_cols: + generate_rows: + col_formats: + - + - + - + - + - + - + tz: UTC + time_profile_id: + display_filter: + col_options: + cpu_allocated_cost: + :grouping: + - :total + cpu_allocated_metric: + :grouping: + - :total + cpu_cost: + :grouping: + - :total + cpu_used_cost: + :grouping: + - :total + cpu_used_metric: + :grouping: + - :total + disk_io_used_cost: + :grouping: + - :total + disk_io_used_metric: + :grouping: + - :total + fixed_compute_metric: + :grouping: + - :total + fixed_compute_1_cost: + :grouping: + - :total + fixed_compute_2_cost: + :grouping: + - :total + fixed_cost: + :grouping: + - :total + fixed_storage_1_cost: + :grouping: + - :total + fixed_storage_2_cost: + :grouping: + - :total + memory_allocated_cost: + :grouping: + - :total + memory_allocated_metric: + :grouping: + - :total + memory_cost: + :grouping: + - :total + memory_used_cost: + :grouping: + - :total + memory_used_metric: + :grouping: + - :total + net_io_used_cost: + :grouping: + - :total + net_io_used_metric: + :grouping: + - :total + storage_allocated_cost: + :grouping: + - :total + storage_allocated_metric: + :grouping: + - :total + storage_cost: + :grouping: + - :total + storage_used_cost: + :grouping: + - :total + storage_used_metric: + :grouping: + - :total + total_cost: + :grouping: + - :total + rpt_options: + :pdf: + :page_size: US Legal - 8.5in x 14.0in + :queue_timeout: + :summary: + :hide_detail_rows: false + miq_group_id: 2 + user_id: 1 + menu_name: Test Report - Chargeback diff --git a/spec/lib/task_helpers/imports/data/reports/Test_Report_attr_error.yml b/spec/lib/task_helpers/imports/data/reports/Test_Report_attr_error.yml new file mode 100644 index 00000000000..5ac3f23d551 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/reports/Test_Report_attr_error.yml @@ -0,0 +1,86 @@ +--- +- MiqReport: + invalid_title: Test Report for Exporting + rpt_group: Custom + rpt_type: Custom + priority: + db: Vm + cols: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + include: {} + col_order: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + headers: + - Name + - IP Addresses + - OS Name + - Number of CPUs + - Cpu Cores Per Socket + - Number of CPU Cores + - Memory + - Evm Owner Name + - Evm Owner Email + conditions: !ruby/object:MiqExpression + exp: + CONTAINS: + tag: Vm.managed-department + value: engineering + context_type: + order: Ascending + sortby: + - evm_owner_email + - name + group: y + graph: + dims: + filename: + file_mtime: + categories: [] + timeline: + template_type: report + where_clause: + db_options: {} + generate_cols: + generate_rows: + col_formats: + - + - + - + - + - + - + - + - + - + tz: + time_profile_id: + display_filter: + col_options: + evm_owner_email: + :break_label: 'Evm Owner Email: ' + :break_format: :model_name + rpt_options: + :pdf: + :page_size: US Letter - 8.5in x 11.0in + :queue_timeout: + :summary: + :hide_detail_rows: false + miq_group_id: 2 + user_id: 1 + menu_name: Test Report diff --git a/spec/lib/task_helpers/imports/data/reports/Test_Report_runtime_error.yml b/spec/lib/task_helpers/imports/data/reports/Test_Report_runtime_error.yml new file mode 100644 index 00000000000..ca14ee3b04c --- /dev/null +++ b/spec/lib/task_helpers/imports/data/reports/Test_Report_runtime_error.yml @@ -0,0 +1,86 @@ +--- +- MiqReporting: + title: Test Report for Exporting + rpt_group: Custom + rpt_type: Custom + priority: + db: Vm + cols: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + include: {} + col_order: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + headers: + - Name + - IP Addresses + - OS Name + - Number of CPUs + - Cpu Cores Per Socket + - Number of CPU Cores + - Memory + - Evm Owner Name + - Evm Owner Email + conditions: !ruby/object:MiqExpression + exp: + CONTAINS: + tag: Vm.managed-department + value: engineering + context_type: + order: Ascending + sortby: + - evm_owner_email + - name + group: y + graph: + dims: + filename: + file_mtime: + categories: [] + timeline: + template_type: report + where_clause: + db_options: {} + generate_cols: + generate_rows: + col_formats: + - + - + - + - + - + - + - + - + - + tz: + time_profile_id: + display_filter: + col_options: + evm_owner_email: + :break_label: 'Evm Owner Email: ' + :break_format: :model_name + rpt_options: + :pdf: + :page_size: US Letter - 8.5in x 11.0in + :queue_timeout: + :summary: + :hide_detail_rows: false + miq_group_id: 2 + user_id: 1 + menu_name: Test Report diff --git a/spec/lib/task_helpers/imports/data/reports/Test_Report_update.yml b/spec/lib/task_helpers/imports/data/reports/Test_Report_update.yml new file mode 100644 index 00000000000..9aab2cab55f --- /dev/null +++ b/spec/lib/task_helpers/imports/data/reports/Test_Report_update.yml @@ -0,0 +1,90 @@ +--- +- MiqReport: + title: Test Report for Exporting Modified + rpt_group: Custom + rpt_type: Custom + priority: + db: Vm + cols: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + - active + include: {} + col_order: + - name + - ipaddresses + - os_image_name + - num_cpu + - cpu_cores_per_socket + - cpu_total_cores + - mem_cpu + - evm_owner_name + - evm_owner_email + - active + headers: + - Name + - IP Addresses + - OS Name + - Number of CPUs + - Cpu Cores Per Socket + - Number of CPU Cores + - Memory + - Evm Owner Name + - Evm Owner Email + - Active + conditions: !ruby/object:MiqExpression + exp: + CONTAINS: + tag: Vm.managed-department + value: engineering + context_type: + order: Ascending + sortby: + - evm_owner_email + - name + group: y + graph: + dims: + filename: + file_mtime: + categories: [] + timeline: + template_type: report + where_clause: + db_options: {} + generate_cols: + generate_rows: + col_formats: + - + - + - + - + - + - + - + - + - + - + tz: + time_profile_id: + display_filter: + col_options: + evm_owner_email: + :break_label: 'Evm Owner Email: ' + :break_format: :model_name + rpt_options: + :pdf: + :page_size: US Letter - 8.5in x 11.0in + :queue_timeout: + :summary: + :hide_detail_rows: false + miq_group_id: 2 + user_id: 1 + menu_name: Test Report diff --git a/spec/lib/task_helpers/imports/reports_spec.rb b/spec/lib/task_helpers/imports/reports_spec.rb new file mode 100644 index 00000000000..f624e443c67 --- /dev/null +++ b/spec/lib/task_helpers/imports/reports_spec.rb @@ -0,0 +1,121 @@ +describe TaskHelpers::Imports::Reports do + describe "#import" do + let(:data_dir) { File.join(File.expand_path(__dir__), 'data', 'reports') } + let(:rpt_file1) { "Test_Report.yaml" } + let(:rpt_file2) { "Test_Report_-_Chargeback.yaml" } + let(:rpt_name1) { "Test Report" } + let(:rpt_name2) { "Test Report - Chargeback" } + let(:rpt_title1) { "Test Report for Exporting" } + let(:rpt_title2) { "Test Chargeback Report" } + let(:rpt_db1) { "Vm" } + let(:rpt_db2) { "ChargebackVm" } + let(:attr_err_file) { "Test_Report_attr_error.yml" } + let(:runt_err_file) { "Test_Report_runtime_error.yml" } + let(:options) { { :source => source, :overwrite => overwrite } } + + before do + FactoryBot.create(:user_admin, :userid => "admin") + end + + describe "when the source is a directory" do + let(:source) { data_dir } + let(:overwrite) { true } + + it 'imports all .yaml files in a specified directory' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to_not output.to_stderr + expect(MiqReport.all.count).to eq(2) + assert_test_rpt_one_present + assert_test_rpt_two_present + end + end + + describe "when the source is a file" do + let(:source) { "#{data_dir}/#{rpt_file1}" } + let(:overwrite) { true } + + it 'imports a specified file' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to_not output.to_stderr + expect(MiqReport.all.count).to eq(1) + assert_test_rpt_one_present + end + end + + describe "when the source file modifies an existing report" do + let(:update_file) { "Test_Report_update.yml" } + let(:source) { "#{data_dir}/#{update_file}" } + + before do + TaskHelpers::Imports::Reports.new.import(:source => "#{data_dir}/#{rpt_file1}") + end + + context 'overwrite is true' do + let(:overwrite) { true } + + it 'overwrites an existing report' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to_not output.to_stderr + assert_test_rpt_one_modified + end + end + + context 'overwrite is false' do + let(:overwrite) { false } + + it 'does not overwrite an existing report' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to_not output.to_stderr + assert_test_rpt_one_present + end + end + end + + describe "when the source file has invalid settings" do + let(:overwrite) { true } + + context "when the object type is invalid" do + let(:source) { "#{data_dir}/#{runt_err_file}" } + + it 'generates an error' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to output(/Incorrect format/).to_stderr + end + end + + context "when an attribute is invalid" do + let(:source) { "#{data_dir}/#{attr_err_file}" } + + it 'generates an error' do + expect do + TaskHelpers::Imports::Reports.new.import(options) + end.to output(/unknown attribute 'invalid_title'/).to_stderr + end + end + end + end + + def assert_test_rpt_one_present + report = MiqReport.find_by(:name => rpt_name1) + expect(report.title).to eq(rpt_title1) + expect(report.db).to eq(rpt_db1) + expect(report.cols).to_not include("active") + end + + def assert_test_rpt_two_present + report = MiqReport.find_by(:name => rpt_name2) + expect(report.title).to eq(rpt_title2) + expect(report.db).to eq(rpt_db2) + end + + def assert_test_rpt_one_modified + report = MiqReport.find_by(:name => rpt_name1) + expect(report.title).to include("Modified") + expect(report.cols).to include("active") + end +end