diff --git a/app/models/generic_object_definition.rb b/app/models/generic_object_definition.rb index cfe35e326b3..8444175ac8c 100644 --- a/app/models/generic_object_definition.rb +++ b/app/models/generic_object_definition.rb @@ -1,4 +1,7 @@ class GenericObjectDefinition < ApplicationRecord + include YAMLImportExportMixin + include_concern 'ImportExport' + TYPE_MAP = { :boolean => ActiveModel::Type::Boolean.new, :datetime => ActiveModel::Type::DateTime.new, diff --git a/app/models/generic_object_definition/import_export.rb b/app/models/generic_object_definition/import_export.rb new file mode 100644 index 00000000000..c5a19afdc7e --- /dev/null +++ b/app/models/generic_object_definition/import_export.rb @@ -0,0 +1,54 @@ +module GenericObjectDefinition::ImportExport + extend ActiveSupport::Concern + + IMPORT_CLASS_NAMES = %w[GenericObjectDefinition].freeze + + module ClassMethods + def import_from_hash(god, options = nil) + raise _("No Generic Object Definition to Import") if god.nil? + if god["name"].blank? || god["properties"].blank? + raise _("Incorrect format.") + end + existing_god = GenericObjectDefinition.find_by(:name => god["name"]) + if existing_god.present? + if options[:overwrite] + # if generic object definition exists, overwrite it's content + msg = "Overwriting Generic Object Definition: [#{existing_god.name}]" + existing_god.attributes = god + result = { + :message => "Replaced Generic Object Definition: [#{god["name"]}]", + :level => :info, + :status => :update + } + else + # if generic object definition exists dont overwrite + msg = "Skipping Generic Object Definition (already in DB): [#{existing_god.name}]" + result = {:message => msg, :level => :error, :status => :skip} + end + else + # create new generic object definition + msg = "Importing Generic Object Definition: [#{god["name"]}]" + existing_god = GenericObjectDefinition.new(god) + result = { + :message => "Imported Generic Object Definition: [#{god["name"]}]", + :level => :info, + :status => :add + } + end + _log.info(msg) + + if result[:status].in?([:add, :update]) + existing_god.save! + _log.info("- Completed.") + end + + return god, result + end + end + + def export_to_array + god_attrs = attributes + ["id", "created_at", "updated_at"].each { |god| god_attrs.delete(god) } + [{self.class.to_s => god_attrs}] + end +end diff --git a/lib/task_helpers/exports/generic_object_definitions.rb b/lib/task_helpers/exports/generic_object_definitions.rb new file mode 100644 index 00000000000..d566f344987 --- /dev/null +++ b/lib/task_helpers/exports/generic_object_definitions.rb @@ -0,0 +1,13 @@ +module TaskHelpers + class Exports + class GenericObjectDefinitions + def export(options = {}) + export_dir = options[:directory] + GenericObjectDefinition.all.each do |god| + filename = Exports.safe_filename(god.name, options[:keep_spaces]) + File.write("#{export_dir}/#{filename}.yaml", god.export_to_array.to_yaml) + end + end + end + end +end diff --git a/lib/task_helpers/imports/generic_object_definitions.rb b/lib/task_helpers/imports/generic_object_definitions.rb new file mode 100644 index 00000000000..c3270bd5be6 --- /dev/null +++ b/lib/task_helpers/imports/generic_object_definitions.rb @@ -0,0 +1,24 @@ +module TaskHelpers + class Imports + class GenericObjectDefinitions + 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 Generic Object Definitions from: #{filename}") + + god_options = {:overwrite => options[:overwrite]} + + begin + god_fd = File.open(filename, 'r') + GenericObjectDefinition.import(god_fd, god_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 2861ea2975e..e1a1d62555c 100644 --- a/lib/tasks/evm_export_import.rake +++ b/lib/tasks/evm_export_import.rake @@ -1,6 +1,7 @@ # Rake script to export and import # * Alerts and AlertSets (Alert Profiles) # * Policies and PolicySets (Policy Profiles) +# * Generic Object Definitions # * Roles # * Tags # * Service Dialogs @@ -114,6 +115,14 @@ namespace :evm do exit # exit so that parameters to the first rake task are not run as rake tasks end + + desc 'Exports all generic object definitions to individual YAML files' + task :generic_object_definitions => :environment do + options = TaskHelpers::Exports.parse_options + TaskHelpers::Exports::GenericObjectDefinitions.new.export(options) + + exit # exit so that parameters to the first rake task are not run as rake tasks + end end namespace :import do @@ -218,5 +227,13 @@ namespace :evm do exit # exit so that parameters to the first rake task are not run as rake tasks end + + desc 'Import all generic object definitions from individual YAML files' + task :generic_object_definitions => :environment do + options = TaskHelpers::Imports.parse_options + TaskHelpers::Imports::GenericObjectDefinitions.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/generic_object_definitions_spec.rb b/spec/lib/task_helpers/exports/generic_object_definitions_spec.rb new file mode 100644 index 00000000000..c883e154ef7 --- /dev/null +++ b/spec/lib/task_helpers/exports/generic_object_definitions_spec.rb @@ -0,0 +1,42 @@ +describe TaskHelpers::Exports::GenericObjectDefinitions do + let(:export_dir) do + Dir.mktmpdir('miq_exp_dir') + end + + after do + FileUtils.remove_entry export_dir + end + + context 'when there is something to export' do + let(:god_filename1) { "#{export_dir}/#{@god1.name}.yaml" } + let(:god_filename2) { "#{export_dir}/#{@god2.name}.yaml" } + + before do + @god1 = FactoryBot.create(:generic_object_definition, :with_methods_attributes_associations, :description => 'god_description') + @god2 = FactoryBot.create(:generic_object_definition) + end + + it 'export all definitions' do + TaskHelpers::Exports::GenericObjectDefinitions.new.export(:directory => export_dir) + expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(2) + god1_yaml = YAML.load_file(god_filename1) + expect(god1_yaml.first["GenericObjectDefinition"]["name"]).to eq(@god1.name) + expect(god1_yaml.first["GenericObjectDefinition"]["description"]).to eq(@god1.description) + expect(god1_yaml.first["GenericObjectDefinition"]["properties"]).to eq(@god1.properties) + + god2_yaml = YAML.load_file(god_filename2) + expect(god2_yaml.first["GenericObjectDefinition"]["name"]).to eq(@god2.name) + expect(god2_yaml.first["GenericObjectDefinition"]["description"]).to eq(nil) + expect(god2_yaml.first["GenericObjectDefinition"]["properties"]).to eq( + :attributes => {}, :associations => {}, :methods => [] + ) + end + end + + context 'when there is nothing to export' do + it 'export no definitions' do + TaskHelpers::Exports::GenericObjectDefinitions.new.export(:directory => export_dir) + expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(0) + end + end +end diff --git a/spec/lib/task_helpers/imports/data/generic_object_definitions/apep.yaml b/spec/lib/task_helpers/imports/data/generic_object_definitions/apep.yaml new file mode 100644 index 00000000000..2697536f1b0 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/generic_object_definitions/apep.yaml @@ -0,0 +1,17 @@ +--- +- GenericObjectDefinition: + name: Apep + description: Ancient Egyptian deity who embodied chaos + properties: + :attributes: + weapon: :string + is_tired: :boolean + created: :datetime + retirement: :datetime + :associations: + cloud_tenant: CloudTenant + :methods: + - kick + - laugh_at + - punch + - parseltongue diff --git a/spec/lib/task_helpers/imports/data/generic_object_definitions/apep_update.yml b/spec/lib/task_helpers/imports/data/generic_object_definitions/apep_update.yml new file mode 100644 index 00000000000..aa7f0cc68b3 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/generic_object_definitions/apep_update.yml @@ -0,0 +1,18 @@ +--- +- GenericObjectDefinition: + name: Apep + description: Updated description + properties: + :attributes: + weapon: :string + is_tired: :boolean + created: :datetime + retirement: :datetime + :associations: + cloud_tenant: CloudTenant + :methods: + - kick + - laugh_at + - punch + - parseltongue + - updated_method diff --git a/spec/lib/task_helpers/imports/data/generic_object_definitions/apophis.yaml b/spec/lib/task_helpers/imports/data/generic_object_definitions/apophis.yaml new file mode 100644 index 00000000000..c91f8bdf698 --- /dev/null +++ b/spec/lib/task_helpers/imports/data/generic_object_definitions/apophis.yaml @@ -0,0 +1,17 @@ +--- +- GenericObjectDefinition: + name: Apophis + description: Ancient Egyptian deity who embodied chaos + properties: + :attributes: + weapon: :string + is_tired: :boolean + created: :datetime + retirement: :datetime + :associations: + cloud_tenant: CloudTenant + :methods: + - kick + - laugh_at + - punch + - parseltongue diff --git a/spec/lib/task_helpers/imports/data/generic_object_definitions/god_attr_error.yml b/spec/lib/task_helpers/imports/data/generic_object_definitions/god_attr_error.yml new file mode 100644 index 00000000000..b8cb25174cc --- /dev/null +++ b/spec/lib/task_helpers/imports/data/generic_object_definitions/god_attr_error.yml @@ -0,0 +1,18 @@ +--- +- GenericObjectDefinition: + name: Apep + invalid_attribute: Apophis + description: Ancient Egyptian deity who embodied chaos + properties: + :attributes: + weapon: :string + is_tired: :boolean + created: :datetime + retirement: :datetime + :associations: + cloud_tenant: CloudTenant + :methods: + - kick + - laugh_at + - punch + - parseltongue diff --git a/spec/lib/task_helpers/imports/data/generic_object_definitions/god_runtime_error.yml b/spec/lib/task_helpers/imports/data/generic_object_definitions/god_runtime_error.yml new file mode 100644 index 00000000000..8f0bab594bb --- /dev/null +++ b/spec/lib/task_helpers/imports/data/generic_object_definitions/god_runtime_error.yml @@ -0,0 +1,5 @@ +--- +- GenericObjectDefinition: + name: + description: Ancient Egyptian deity who embodied chaos + properties: diff --git a/spec/lib/task_helpers/imports/generic_object_definitions_spec.rb b/spec/lib/task_helpers/imports/generic_object_definitions_spec.rb new file mode 100644 index 00000000000..c0dbfe0fca0 --- /dev/null +++ b/spec/lib/task_helpers/imports/generic_object_definitions_spec.rb @@ -0,0 +1,130 @@ +describe TaskHelpers::Imports::GenericObjectDefinitions do + describe "#import" do + let(:data_dir) { File.join(File.expand_path(__dir__), 'data', 'generic_object_definitions') } + let(:options) { { :source => source, :overwrite => overwrite } } + let(:god_name1) { "Apep" } + let(:god_name2) { "Apophis" } + let(:god_file1) { "apep.yaml" } + let(:god_file2) { "apophis.yaml" } + let(:god_desc1) { "Ancient Egyptian deity who embodied chaos" } + let(:god_desc1_updated) { "Updated description" } + let(:runt_err_file) { "god_runtime_error.yml" } + let(:attr_err_file) { "god_attr_error.yml" } + let(:god_prop1) do + { + :attributes => { + 'weapon' => :string, + 'is_tired' => :boolean, + 'created' => :datetime, + 'retirement' => :datetime + }, + :associations => { 'cloud_tenant' => 'CloudTenant' }, + :methods => ['kick', 'laugh_at', 'punch', 'parseltongue'] + } + 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::GenericObjectDefinitions.new.import(options) + end.to_not output.to_stderr + expect(GenericObjectDefinition.all.count).to eq(2) + assert_test_god_one_present + assert_test_god_two_present + end + end + + describe "when the source is a file" do + let(:source) { "#{data_dir}/#{god_file1}" } + let(:overwrite) { true } + + it 'imports a specified file' do + expect do + TaskHelpers::Imports::GenericObjectDefinitions.new.import(options) + end.to_not output.to_stderr + expect(GenericObjectDefinition.all.count).to eq(1) + assert_test_god_one_present + end + end + + describe "when the source file modifies an existing generic object definition" do + let(:update_file) { "apep_update.yml" } + let(:source) { "#{data_dir}/#{update_file}" } + + before do + TaskHelpers::Imports::GenericObjectDefinitions.new.import(:source => "#{data_dir}/#{god_file1}") + end + + context 'overwrite is true' do + let(:overwrite) { true } + + it 'overwrites an existing generic object definition' do + expect do + TaskHelpers::Imports::GenericObjectDefinitions.new.import(options) + end.to_not output.to_stderr + assert_test_god_one_modified + end + end + + context 'overwrite is false' do + let(:overwrite) { false } + + it 'does not overwrite an existing generic object definition' do + expect do + TaskHelpers::Imports::GenericObjectDefinitions.new.import(options) + end.to_not output.to_stderr + assert_test_god_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::GenericObjectDefinitions.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::GenericObjectDefinitions.new.import(options) + end.to output(/unknown attribute 'invalid_attribute'/).to_stderr + end + end + end + end + + def assert_test_god_one_present + god = GenericObjectDefinition.find_by(:name => god_name1) + expect(god.name).to eq(god_name1) + expect(god.description).to eq(god_desc1) + expect(god.properties).to eq(god_prop1) + end + + def assert_test_god_two_present + god = GenericObjectDefinition.find_by(:name => god_name2) + expect(god.name).to eq(god_name2) + expect(god.description).to eq(god_desc1) + expect(god.properties).to eq(god_prop1) + end + + def assert_test_god_one_modified + god = GenericObjectDefinition.find_by(:name => god_name1) + expect(god.name).to eq(god_name1) + expect(god.description).to eq(god_desc1_updated) + god_prop1[:methods] << 'updated_method' + expect(god.properties).to eq(god_prop1) + end +end