Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for exporting and importing customization templates #17877

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions lib/task_helpers/exports/customization_templates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module TaskHelpers
class Exports
class CustomizationTemplates
EXCLUDE_ATTRS = %i(created_at updated_at id pxe_image_type_id class).freeze
def export(options = {})
export_dir = options[:directory]

customization_templates = options[:all] ? CustomizationTemplate.all : CustomizationTemplate.where(:system => [false, nil])

customization_templates.order(:id).each do |customization_template|
$log.info("Exporting Customization Template: #{customization_template.name} (ID: #{customization_template.id})")

ct_hash = Exports.exclude_attributes(customization_template.to_model_hash, EXCLUDE_ATTRS)
ct_hash.merge!(pxe_image_type_hash(customization_template.pxe_image_type))

image_type_name = ct_hash.fetch_path(:pxe_image_type, :name) || "Examples"
filename = Exports.safe_filename("#{image_type_name}-#{ct_hash[:name]}", options[:keep_spaces])
File.write("#{export_dir}/#{filename}.yaml", ct_hash.to_yaml)
end
end

private

def pxe_image_type_hash(pxe_image_type)
if pxe_image_type
{ :pxe_image_type => pxe_image_type.to_model_hash.reject { |key| EXCLUDE_ATTRS.include?(key) } }
else
{ :pxe_image_type => {} }
end
end
end
end
end
90 changes: 90 additions & 0 deletions lib/task_helpers/imports/customization_templates.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
module TaskHelpers
class Imports
class CustomizationTemplates
class CustomizationTemplateYamlError < StandardError
attr_accessor :details

def initialize(message = nil, details = nil)
super(message)
self.details = details
end
end

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 Customization Template from: #{filename}")

begin
custom_template_hash = YAML.load_file(filename)
import_customization_template(custom_template_hash)
rescue CustomizationTemplateYamlError => err
$log.error("Error importing #{filename} : #{err.message}")
warn("Error importing #{filename} : #{err.message}")
err.details.each do |detail|
$log.error(detail.to_s)
warn("\t#{detail}")
end
end
end
end

private

def import_customization_template(custom_template_hash)
CustomizationTemplate.transaction do
unless valid_type?(custom_template_hash[:type])
raise CustomizationTemplateYamlError.new("Customization Template error",
["Invalid type: #{custom_template_hash[:type]}"])
end

if custom_template_hash[:system]
raise CustomizationTemplateYamlError.new("Customization Template error",
["Cannot import because :system is set to true"])
end

pxe_image_type_hash = custom_template_hash.delete(:pxe_image_type)
custom_template_hash[:pxe_image_type] = get_pxe_image_type(pxe_image_type_hash)

customization_template = CustomizationTemplate.find_by(:name => custom_template_hash[:name],
:pxe_image_type => custom_template_hash[:pxe_image_type])

if customization_template
customization_template.update(custom_template_hash)
else
imported_ct = CustomizationTemplate.create(custom_template_hash)

unless imported_ct.valid?
raise CustomizationTemplateYamlError.new("Customization Template error",
imported_ct.errors.full_messages)
end
end
end
end

def get_pxe_image_type(pxe_image_hash)
unless pxe_image_hash.key?(:name)
raise CustomizationTemplateYamlError.new("Customization Template error",
["Cannot import because there is no :name for :pxe_image_type"])
end

if pxe_image_hash.key?(:provision_type) && !%w(vm host).include?(pxe_image_hash[:provision_type])
raise CustomizationTemplateYamlError.new("Customization Template error",
["Cannot import because :provision_type for :pxe_image_type must be vm or host"])
end

pit = PxeImageType.find_or_create_by(pxe_image_hash)

raise CustomizationTemplateYamlError.new("Customization Template error", pit.errors.full_messages) unless pit.valid?

pit
end

def valid_type?(custom_template_type)
CustomizationTemplate.descendants.collect(&:name).include?(custom_template_type)
end
end
end
end
19 changes: 19 additions & 0 deletions lib/tasks/evm_export_import.rake
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
# * Tags
# * Service Dialogs
# * Provision Dialogs
# * Custom Buttons
# * SmartState Analysis Scan Profiles
# * Customization Templates

namespace :evm do
namespace :export do
Expand Down Expand Up @@ -85,6 +88,14 @@ namespace :evm do

exit # exit so that parameters to the first rake task are not run as rake tasks
end

desc 'Exports all customization templates to individual YAML files'
task :customization_templates => :environment do
options = TaskHelpers::Exports.parse_options
TaskHelpers::Exports::CustomizationTemplates.new.export(options)

exit # exit so that parameters to the first rake task are not run as rake tasks
end
end

namespace :import do
Expand Down Expand Up @@ -165,5 +176,13 @@ namespace :evm do

exit # exit so that parameters to the first rake task are not run as rake tasks
end

desc 'Imports all customization templates from individual YAML files'
task :customization_templates => :environment do
options = TaskHelpers::Imports.parse_options
TaskHelpers::Imports::CustomizationTemplates.new.import(options)

exit # exit so that parameters to the first rake task are not run as rake tasks
end
end
end
107 changes: 107 additions & 0 deletions spec/lib/task_helpers/exports/customization_templates_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
describe TaskHelpers::Exports::CustomizationTemplates do
let(:template_name) { "Basic root pass template" }
let(:template_type) { "CustomizationTemplateCloudInit" }
let(:template_desc) { "This template takes use of rootpassword defined in the UI" }
let(:template_script) { "#cloud-config\nchpasswd:\n list: |\n root:<%= MiqPassword.decrypt(evm[:root_password]) %>\n expire: False" }
let(:image_type_name1) { "CentOS-6" }
let(:image_type_name2) { "RHEL-7" }
let(:provision_type2) { "vm" }

let(:content1) do
{ :name => template_name,
:description => template_desc,
:script => template_script,
:type => template_type,
:pxe_image_type => {
:name => image_type_name1
} }
end

let(:content2) do
{ :name => template_name,
:description => template_desc,
:script => template_script,
:type => template_type,
:pxe_image_type => {
:name => image_type_name2,
:provision_type => provision_type2
} }
end

let(:content3) do
{ :name => template_name,
:description => template_desc,
:script => template_script,
:type => template_type,
:system => true,
:pxe_image_type => {} }
end

let(:export_dir) do
Dir.mktmpdir('miq_exp_dir')
end

before do
pit1 = FactoryGirl.create(:pxe_image_type,
:name => image_type_name1)

pit2 = FactoryGirl.create(:pxe_image_type,
:name => image_type_name2,
:provision_type => provision_type2)

FactoryGirl.create(:customization_template,
:name => template_name,
:type => template_type,
:description => template_desc,
:script => template_script,
:pxe_image_type => pit1)

FactoryGirl.create(:customization_template,
:name => template_name,
:type => template_type,
:description => template_desc,
:script => template_script,
:pxe_image_type => pit2)

CustomizationTemplate.create!(:name => template_name,
:type => template_type,
:description => template_desc,
:system => true,
:script => template_script)
end

after do
FileUtils.remove_entry export_dir
end

describe "when --all is not specified" do
let(:template_filename1) { "#{export_dir}/#{image_type_name1}-Basic_root_pass_template.yaml" }
let(:template_filename2) { "#{export_dir}/#{image_type_name2}-Basic_root_pass_template.yaml" }

it 'exports user customization templates to a given directory with unique filenames' do
TaskHelpers::Exports::CustomizationTemplates.new.export(:directory => export_dir)
expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(2)
customization_template1 = YAML.load_file(template_filename1)
expect(customization_template1).to eq(content1)
customization_template2 = YAML.load_file(template_filename2)
expect(customization_template2).to eq(content2)
end
end

describe "when --all is specified" do
let(:template_filename1) { "#{export_dir}/#{image_type_name1}-Basic_root_pass_template.yaml" }
let(:template_filename2) { "#{export_dir}/#{image_type_name2}-Basic_root_pass_template.yaml" }
let(:template_filename3) { "#{export_dir}/Examples-Basic_root_pass_template.yaml" }

it 'exports all provision dialogs to a given directory with unique filenames' do
TaskHelpers::Exports::CustomizationTemplates.new.export(:directory => export_dir, :all => true)
expect(Dir[File.join(export_dir, '**', '*')].count { |file| File.file?(file) }).to eq(3)
customization_template1 = YAML.load_file(template_filename1)
expect(customization_template1).to eq(content1)
customization_template2 = YAML.load_file(template_filename2)
expect(customization_template2).to eq(content2)
customization_template3 = YAML.load_file(template_filename3)
expect(customization_template3).to eq(content3)
end
end
end
122 changes: 122 additions & 0 deletions spec/lib/task_helpers/imports/customization_templates_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
describe TaskHelpers::Imports::CustomizationTemplate do
describe "#import" do
let(:data_dir) { File.join(File.expand_path(__dir__), 'data', 'customization_templates') }
let(:ct_file) { "existing_ct_and_pit.yaml" }
let(:ct_name) { "Basic root pass template" }
let(:ct_desc) { "This template takes use of rootpassword defined in the UI" }
let(:existing_pit_name) { "RHEL-6" }
let(:new_pit_name) { "RHEL-7" }
let(:new_pit_pt) { "vm" }
let(:options) { { :source => source } }

describe "when the source is a directory" do
let(:source) { data_dir }

it 'imports all .yaml files in a specified directory' do
pit = PxeImageType.create(:name => existing_pit_name)
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to_not output.to_stderr
assert_test_ct_one_present(pit)
assert_test_ct_two_present
end
end

describe "when the source is a file" do
let(:source) { "#{data_dir}/#{ct_file}" }

it 'imports a specified file' do
pit = PxeImageType.create(:name => existing_pit_name)
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to_not output.to_stderr
assert_test_ct_one_present(pit)
end
end

describe "when the source file modifies an existing file" do
let(:update_file) { "update_existing.yml" }
let(:source) { "#{data_dir}/#{update_file}" }

before do
TaskHelpers::Imports::CustomizationTemplates.new.import(:source => "#{data_dir}/#{ct_file}")
end

it 'modifies an existing customization template' do
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to_not output.to_stderr
assert_test_ct_one_modified
end
end

describe "when the source file has invalid settings" do
describe "when :system is true" do
let(:ct_system_file) { "system_ct.yml" }
let(:source) { "#{data_dir}/#{ct_system_file}" }

it 'fails to import' do
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to output.to_stderr
end
end

describe "when there is no :pxe_image_type[:name]" do
let(:no_pit_name_file) { "no_pit_name.yml" }
let(:source) { "#{data_dir}/#{no_pit_name_file}" }

it 'fails to import' do
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to output.to_stderr
end
end

describe "when the :pxe_image_type[:provision_type] is invalid" do
let(:invalid_pt_file) { "invalid_pit_pt.yml" }
let(:source) { "#{data_dir}/#{invalid_pt_file}" }

it 'fails to import' do
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to output.to_stderr
end
end

describe "when the :pxe_image_type[:name] is found with a different provision_type"
let(:diff_pt_file) { "pit_existing_name_new_pt.yml" }
let(:source) { "#{data_dir}/#{diff_pt_file}" }

before do
PxeImageType.create(:name => existing_pit_name)
end

it 'fails to import' do
expect do
TaskHelpers::Imports::CustomizationTemplates.new.import(options)
end.to output.to_stderr
end
end
end

def assert_test_ct_one_present(pit)
custom_template = CustomizationTemplate.find_by(:name => ct_name, :pxe_image_type => pit)
expect(custom_template.description).to eq(ct_desc)
expect(custom_template.pxe_image_type).to eq(pit)
end

def assert_test_ct_two_present
pit = PxeImageType.find_by(:name => new_pit_name, :provision_type => new_pit_pt)
custom_template = CustomizationTemplate.find_by(:name => ct_name, :pxe_image_type => pit)
expect(custom_template.description).to eq(ct_desc)
expect(custom_template.pxe_image_type).to eq(pit)
end

def assert_test_ct_one_modified
pit = PxeImageType.find_by(:name => existing_pit_name)
custom_template = CustomizationTemplate.find_by(:name => ct_name, :pxe_image_type => pit)
expect(custom_template.description).to include("updated")
expect(custom_template.script).to include("This line added")
end
end
Loading