diff --git a/modules/tftp/server.rb b/modules/tftp/server.rb index 98218d8bd..32e7f83e4 100644 --- a/modules/tftp/server.rb +++ b/modules/tftp/server.rb @@ -63,6 +63,19 @@ def delete_file(file) logger.debug "TFTP: Skipping a request to delete a file which doesn't exists" end end + + def delete_host_dir(mac) + host_dir = File.join(path, 'host-config', dashed_mac(mac).downcase) + logger.debug "TFTP: Removing directory '#{host_dir}'." + FileUtils.rm_rf host_dir + end + + def setup_bootloader(mac:, os:, release:, arch:, bootfile_suffix:) + end + + def dashed_mac(mac) + mac.tr(':', '-') + end end class Syslinux < Server @@ -75,7 +88,7 @@ def pxe_default end def pxeconfig_file(mac) - ["#{pxeconfig_dir}/01-" + mac.tr(':', "-").downcase] + ["#{pxeconfig_dir}/01-" + dashed_mac(mac).downcase] end end class Pxelinux < Syslinux; end @@ -90,13 +103,96 @@ def pxe_default end def pxeconfig_file(mac) - ["#{pxeconfig_dir}/menu.lst.01" + mac.delete(':').upcase, "#{pxeconfig_dir}/01-" + mac.tr(':', '-').upcase] + ["#{pxeconfig_dir}/menu.lst.01" + mac.delete(':').upcase, "#{pxeconfig_dir}/01-" + dashed_mac(mac).upcase] end end class Pxegrub2 < Server - def pxeconfig_dir - "#{path}/grub2" + def bootloader_path(os, release, arch) + [release, "default"].each do |version| + bootloader_path = File.join(path, 'bootloader-universe/pxegrub2', os, version, arch) + + logger.debug "TFTP: Checking if bootloader universe is configured for OS '#{os}' version '#{version}' (#{arch})." + + if Dir.exist?(bootloader_path) + logger.debug "TFTP: Directory '#{bootloader_path}' exists." + return bootloader_path + end + + logger.debug "TFTP: Directory '#{bootloader_path}' does not exist." + end + nil + end + + def bootloader_universe_symlinks(bootloader_path, pxeconfig_dir_mac) + Dir.glob(File.join(bootloader_path, '*.efi')).map do |source_file| + { source: source_file, symlink: File.join(pxeconfig_dir_mac, File.basename(source_file)) } + end + end + + def default_symlinks(bootfile_suffix, pxeconfig_dir_mac) + pxeconfig_dir = pxeconfig_dir() + + grub_source = "grub#{bootfile_suffix}.efi" + shim_source = "shim#{bootfile_suffix}.efi" + + [ + { source: grub_source, symlink: "boot.efi" }, + { source: grub_source, symlink: grub_source }, + { source: shim_source, symlink: "boot-sb.efi" }, + { source: shim_source, symlink: shim_source }, + ].map do |link| + { source: File.join(pxeconfig_dir, link[:source]), symlink: File.join(pxeconfig_dir_mac, link[:symlink]) } + end + end + + def create_symlinks(symlinks) + symlinks.each do |link| + relative_source_path = Pathname.new(link[:source]).relative_path_from(Pathname.new(link[:symlink]).parent).to_s + + logger.debug "TFTP: Creating relative symlink: #{link[:symlink]} -> #{relative_source_path}" + FileUtils.ln_s(relative_source_path, link[:symlink], force: true) + end + end + + # Configures bootloader files for a host in its host-config directory + # + # @param mac [String] The MAC address of the host + # @param os [String] The lowercase name of the operating system of the host + # @param release [String] The major and minor version of the operating system of the host + # @param arch [String] The architecture of the operating system of the host + # @param bootfile_suffix [String] The architecture specific boot filename suffix + def setup_bootloader(mac:, os:, release:, arch:, bootfile_suffix:) + pxeconfig_dir_mac = pxeconfig_dir(mac) + + logger.debug "TFTP: Deploying host specific bootloader files to '#{pxeconfig_dir_mac}'." + + FileUtils.mkdir_p(pxeconfig_dir_mac) + FileUtils.rm_f(Dir.glob("#{pxeconfig_dir_mac}/*.efi")) + + bootloader_path = bootloader_path(os, release, arch) + + if bootloader_path + logger.debug "TFTP: Creating symlinks from bootloader universe." + symlinks = bootloader_universe_symlinks(bootloader_path, pxeconfig_dir_mac) + else + logger.debug "TFTP: Creating symlinks from default bootloader files." + symlinks = default_symlinks(bootfile_suffix, pxeconfig_dir_mac) + end + create_symlinks(symlinks) + end + + def del(mac) + super mac + delete_host_dir mac + end + + def pxeconfig_dir(mac = nil) + if mac + File.join(path, 'host-config', dashed_mac(mac).downcase, 'grub2') + else + File.join(path, 'grub2') + end end def pxe_default @@ -104,7 +200,14 @@ def pxe_default end def pxeconfig_file(mac) - ["#{pxeconfig_dir}/grub.cfg-01-" + mac.tr(':', '-').downcase, "#{pxeconfig_dir}/grub.cfg-#{mac.downcase}"] + pxeconfig_dir_mac = pxeconfig_dir(mac) + [ + "#{pxeconfig_dir_mac}/grub.cfg", + "#{pxeconfig_dir_mac}/grub.cfg-01-#{dashed_mac(mac).downcase}", + "#{pxeconfig_dir_mac}/grub.cfg-#{mac.downcase}", + "#{pxeconfig_dir}/grub.cfg-01-" + dashed_mac(mac).downcase, + "#{pxeconfig_dir}/grub.cfg-#{mac.downcase}", + ] end end @@ -146,7 +249,7 @@ def pxe_default end def pxeconfig_file(mac) - ["#{pxeconfig_dir}/01-" + mac.tr(':', "-").downcase + ".ipxe"] + ["#{pxeconfig_dir}/01-" + dashed_mac(mac).downcase + ".ipxe"] end end diff --git a/modules/tftp/tftp_api.rb b/modules/tftp/tftp_api.rb index 1bc6276c7..02f3b2948 100644 --- a/modules/tftp/tftp_api.rb +++ b/modules/tftp/tftp_api.rb @@ -18,8 +18,9 @@ def instantiate(variant, mac = nil) Object.const_get("Proxy").const_get('TFTP').const_get(variant.capitalize).new end - def create(variant, mac) + def create(variant, mac, os: nil, release: nil, arch: nil, bootfile_suffix: nil) tftp = instantiate variant, mac + log_halt(400, "TFTP: Failed to setup host specific bootloader directory: ") { tftp.setup_bootloader(mac: mac, os: os, release: release, arch: arch, bootfile_suffix: bootfile_suffix) } log_halt(400, "TFTP: Failed to create pxe config file: ") { tftp.set(mac, (params[:pxeconfig] || params[:syslinux_config])) } end @@ -48,7 +49,7 @@ def create_default(variant) end post "/:variant/:mac" do |variant, mac| - create variant, mac + create variant, mac, os: params[:targetos], release: params[:release], arch: params[:arch], bootfile_suffix: params[:bootfile_suffix] end delete "/:variant/:mac" do |variant, mac| diff --git a/modules/tftp/tftp_plugin.rb b/modules/tftp/tftp_plugin.rb index d4dc06450..bb92bed53 100644 --- a/modules/tftp/tftp_plugin.rb +++ b/modules/tftp/tftp_plugin.rb @@ -2,6 +2,8 @@ module Proxy::TFTP class Plugin < ::Proxy::Plugin plugin :tftp, ::Proxy::VERSION + capability :bootloader_universe + rackup_path File.expand_path("http_config.ru", __dir__) default_settings :tftproot => '/var/lib/tftpboot', diff --git a/test/tftp/integration_test.rb b/test/tftp/integration_test.rb index a9b88a8f8..9b621e7f9 100644 --- a/test/tftp/integration_test.rb +++ b/test/tftp/integration_test.rb @@ -14,7 +14,7 @@ def test_features mod = response['tftp'] refute_nil(mod) assert_equal('running', mod['state'], Proxy::LogBuffer::Buffer.instance.info[:failed_modules][:tftp]) - assert_equal([], mod['capabilities']) + assert_equal(["bootloader_universe"], mod['capabilities']) expected_settings = { 'tftp_servername' => 'tftp.example.com' } assert_equal(expected_settings, mod['settings']) diff --git a/test/tftp/tftp_server_test.rb b/test/tftp/tftp_server_test.rb index 42d6f1071..80a8c5c41 100644 --- a/test/tftp/tftp_server_test.rb +++ b/test/tftp/tftp_server_test.rb @@ -46,6 +46,10 @@ def test_create_default end @subject.create_default @content end + + def test_dashed_mac + assert "aa-bb-cc-dd-ee-ff", @subject.dashed_mac(@mac) + end end class HelperServerTest < Test::Unit::TestCase @@ -111,11 +115,90 @@ def setup_paths class TftpPxegrub2ServerTest < Test::Unit::TestCase include TftpGenericServerSuite + def setup + @arch = "x86_64" + @bootfile_suffix = "x64" + @os = "redhat" + @release = "9.4" + super + end + def setup_paths @subject = Proxy::TFTP::Pxegrub2.new - @pxe_config_files = ["grub2/grub.cfg-01-aa-bb-cc-dd-ee-ff", "grub2/grub.cfg-aa:bb:cc:dd:ee:ff"] + @pxe_config_files = [ + "host-config/aa-bb-cc-dd-ee-ff/grub2/grub.cfg", + "host-config/aa-bb-cc-dd-ee-ff/grub2/grub.cfg-01-aa-bb-cc-dd-ee-ff", + "host-config/aa-bb-cc-dd-ee-ff/grub2/grub.cfg-aa:bb:cc:dd:ee:ff", + "grub2/grub.cfg-01-aa-bb-cc-dd-ee-ff", + "grub2/grub.cfg-aa:bb:cc:dd:ee:ff", + ] @pxe_default_files = ["grub2/grub.cfg"] end + + def test_pxeconfig_dir + assert_equal File.join(@subject.path, "host-config", @subject.dashed_mac(@mac).downcase, "grub2"), @subject.pxeconfig_dir(@mac) + assert_equal File.join(@subject.path, "grub2"), @subject.pxeconfig_dir() + end + + def test_release_specific_bootloader_path + release_specific_bootloader_path = File.join(@subject.path, "bootloader-universe/pxegrub2", @os, @release, @arch) + Dir.stubs(:exist?).with(release_specific_bootloader_path).returns(true).once + assert_equal release_specific_bootloader_path, @subject.bootloader_path(@os, @release, @arch) + end + + def test_default_bootloader_path + release_specific_bootloader_path = File.join(@subject.path, "bootloader-universe/pxegrub2", @os, @release, @arch) + default_bootloader_path = File.join(@subject.path, "bootloader-universe/pxegrub2", @os, "default", @arch) + Dir.stubs(:exist?).with(release_specific_bootloader_path).returns(false).once + Dir.stubs(:exist?).with(default_bootloader_path).returns(true).once + assert_equal default_bootloader_path, @subject.bootloader_path(@os, @release, @arch) + end + + def test_bootloader_universe_symlinks + temp_dir = Dir.mktmpdir() + pxeconfig_dir_mac = @subject.pxeconfig_dir(@mac) + symlinks = [ + { source: File.join(temp_dir, "dummy1.efi"), symlink: File.join(pxeconfig_dir_mac, "dummy1.efi") }, + { source: File.join(temp_dir, "dummy2.efi"), symlink: File.join(pxeconfig_dir_mac, "dummy2.efi") }, + ] + symlinks.each do |entry| + FileUtils.touch(entry[:source]) + end + assert_equal( + symlinks.sort_by { |entry| entry[:source] }, + @subject.bootloader_universe_symlinks(temp_dir, pxeconfig_dir_mac).sort_by { |entry| entry[:source] } + ) + end + + def test_default_symlinks + pxeconfig_dir = @subject.pxeconfig_dir + pxeconfig_dir_mac = @subject.pxeconfig_dir(@mac) + symlinks = [ + { source: File.join(pxeconfig_dir, "grub#{@bootfile_suffix}.efi"), symlink: File.join(pxeconfig_dir_mac, "boot.efi") }, + { source: File.join(pxeconfig_dir, "grub#{@bootfile_suffix}.efi"), symlink: File.join(pxeconfig_dir_mac, "grub#{@bootfile_suffix}.efi") }, + { source: File.join(pxeconfig_dir, "shim#{@bootfile_suffix}.efi"), symlink: File.join(pxeconfig_dir_mac, "boot-sb.efi") }, + { source: File.join(pxeconfig_dir, "shim#{@bootfile_suffix}.efi"), symlink: File.join(pxeconfig_dir_mac, "shim#{@bootfile_suffix}.efi") }, + ] + assert_equal symlinks, @subject.default_symlinks(@bootfile_suffix, @subject.pxeconfig_dir(@mac)) + end + + def test_create_symlinks + symlinks = [ + { source: "/path/to/source1", symlink: "/path/to/symlink1" }, + { source: "/path/to/source2", symlink: "/another/path/to/symlink2" }, + ] + FileUtils.expects(:ln_s).with("source1", "/path/to/symlink1", {:force => true}).once + FileUtils.expects(:ln_s).with("../../../path/to/source2", "/another/path/to/symlink2", {:force => true}).once + @subject.create_symlinks(symlinks) + end + + def test_del + pxe_config_files.each do |file| + @subject.expects(:delete_file).with(file).once + end + @subject.expects(:delete_host_dir).with(@mac).once + @subject.del @mac + end end class TftpPoapServerTest < Test::Unit::TestCase