Class: ForemanBootdisk::ISOGenerator

Inherits:
Object
  • Object
show all
Extended by:
Foreman::HttpProxy
Defined in:
app/services/foreman_bootdisk/iso_generator.rb

Class Method Summary collapse

Class Method Details

.build_mkiso_command(output_file:, source_directory:, extra_commands:) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 176

def self.build_mkiso_command(output_file:, source_directory:, extra_commands:)
  arguments = [
    "-o #{output_file}",
    '-iso-level 2',
    '-b isolinux.bin',
    '-c boot.cat',
    '-no-emul-boot',
    '-boot-load-size 4',
    '-boot-info-table'
  ]
  arguments.push(extra_commands) unless extra_commands.nil?
  [Setting[:bootdisk_mkiso_command], arguments, source_directory].flatten.join(' ')
end

.fetch(path, uri, limit = 10) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 197

def self.fetch(path, uri, limit = 10)
  dir = File.dirname(path)
  FileUtils.mkdir_p(dir) unless File.exist?(dir)

  use_cache = !!Setting[:bootdisk_cache_media]
  write_cache = false
  File.open(path, 'w') do |file|
    file.binmode

    if use_cache && !(contents = Rails.cache.fetch(uri, raw: true)).nil?
      ForemanBootdisk.logger.info("Retrieved #{uri} from local cache (use foreman-rake tmp:cache:clear to empty)")
      file.write(contents)
    else
      ForemanBootdisk.logger.info("Fetching #{uri}")
      write_cache = use_cache
      uri = URI(uri)

      if proxy_http_request?(nil, uri.host, uri.scheme)
        proxy_uri = URI.parse(http_proxy)
        http_object = Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
      else
        http_object = Net::HTTP
      end
      http_object.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
        request = Net::HTTP::Get.new(uri.request_uri, 'Accept-Encoding' => 'plain')

        http.request(request) do |response|
          case response
          when Net::HTTPSuccess then
            response.read_body do |chunk|
              file.write chunk
            end
          when Net::HTTPRedirection then
            raise("Too many HTTP redirects when downloading #{uri}") if limit <= 0

            fetch(path, response['location'], limit - 1)
            # prevent multiple writes to the cache
            write_cache = false
          else
            raise ::Foreman::Exception, N_(format("Unable to download boot file %{uri}, HTTP return code %{code}", uri: uri, code: response.code))
          end
        end
      end
    end
  end

  return unless write_cache

  contents = File.read(path)
  return if contents.empty?

  ForemanBootdisk.logger.debug("Caching contents of #{uri}")
  Rails.cache.write(uri, contents, raw: true)
end

.generate(opts = {}) ⇒ Object



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 65

def self.generate(opts = {})
  # isolinux template not provided for generic BIOS bootdisks
  if opts[:isolinux].nil? && opts[:ipxe]
    opts[:isolinux] = <<~EOT
      default ipxe
      label ipxe
      kernel /ipxe
      initrd /script
    EOT
  end

  if opts[:grub].nil? && opts[:ipxe]
    opts[:grub] = <<~EOT
      echo "Grub2 template not associated and/or unsupported bootdisk type."
      echo "The following bootdisks are supported for EFI systems:"
      echo " * FULL HOST BOOTDISK"
      echo " * SUBNET GENERIC BOOTDISK"
      echo "The system will poweroff in few minutes..."
      sleep 500
      poweroff
    EOT
  end

  # And create new temporary directory:
  wd = Dir.mktmpdir('bootdisk-iso-', Rails.root.join('tmp'))
  Dir.mkdir(File.join(wd, 'build'))

  if opts[:isolinux]
    isolinux_source_file = File.join(Setting[:bootdisk_isolinux_dir], 'isolinux.bin')
    raise Foreman::Exception.new(N_('Please ensure the isolinux/syslinux package(s) are installed.')) unless File.exist?(isolinux_source_file)

    FileUtils.cp(isolinux_source_file, File.join(wd, 'build', 'isolinux.bin'))

    source_files = ['ldlinux.c32', 'menu.c32', 'libutil.c32']
    source_files.each do |source_file|
      full_path = File.join(Setting[:bootdisk_syslinux_dir], source_file)
      FileUtils.cp(full_path, File.join(wd, 'build', source_file)) if File.exist?(full_path)
    end

    File.open(File.join(wd, 'build', 'isolinux.cfg'), 'w') do |file|
      file.write(opts[:isolinux])
    end
  end

  if opts[:ipxe]
    ipxe_source_file = File.join(Setting[:bootdisk_ipxe_dir], 'ipxe.lkrn')
    raise Foreman::Exception.new(N_('Please ensure the ipxe-bootimgs package is installed.')) unless File.exist?(ipxe_source_file)

    FileUtils.cp(ipxe_source_file, File.join(wd, 'build', 'ipxe'))
    File.open(File.join(wd, 'build', 'script'), 'w') { |file| file.write(opts[:ipxe]) }
  end

  if opts[:grub]
    FileUtils.mkdir_p(File.join(wd, 'build', 'EFI', 'BOOT'))
    # a copy when using CDROM directly
    File.open(File.join(wd, 'build', 'EFI', 'BOOT', 'grub.cfg'), 'w') { |file| file.write(opts[:grub]) }
    # a copy when using USB/HDD (copied into EFI image as well)
    File.open(File.join(wd, 'build', 'grub-hdd.cfg'), 'w') do |f|
      # set root to (cd0) or similar when booting via disk
      f.write("search --file /ISOLINUX.BIN --set root\n")
      f.write(opts[:grub])
    end
    efibootimg = File.join(wd, 'build', 'efiboot.img')
    system_or_exception("truncate -s 6M #{efibootimg}", N_('Creating new image failed, install truncate utility'))
    system_or_exception("mkfs.msdos #{efibootimg}", N_('Failed to format the ESP image via mkfs.msdos'))
    system_or_exception("mmd -i #{efibootimg} '::/EFI'", N_('Failed to create a directory within the ESP image'))
    system_or_exception("mmd -i #{efibootimg} '::/EFI/BOOT'", N_('Failed to create a directory within the ESP image'))
    {
      File.join(wd, 'build', 'grub-hdd.cfg').to_s => 'GRUB.CFG',
      File.join(Setting[:bootdisk_grub2_dir], 'grubx64.efi').to_s => 'GRUBX64.EFI',
      File.join(Setting[:bootdisk_grub2_dir], 'shimx64.efi').to_s => 'BOOTX64.EFI',
    }.each do |src, dest|
      raise(Foreman::Exception.new(N_('Ensure %{file} is readable (or update "Grub2 directory" setting)'), file: src)) unless File.exist?(src)
      raise(Foreman::Exception.new(N_('Unable to mcopy %{file}'), file: src)) unless system("mcopy -m -i #{efibootimg} '#{src}' '#{"::/EFI/BOOT/#{dest}"}'")
    end
    mkiso_args = "-eltorito-alt-boot -e efiboot.img -no-emul-boot"
    isohybrod_args = ["--uefi"]
  else
    mkiso_args = ''
    isohybrod_args = []
  end

  if opts[:files]
    if opts[:files].respond_to?(:each)
      opts[:files].each do |file, source|
        fetch(File.join(wd, 'build', file), source)
      end
    end
  end

  iso = if opts[:dir]
          Tempfile.new(['bootdisk', '.iso'], opts[:dir]).path
        else
          File.join(wd, 'output.iso')
        end
  raise Foreman::Exception.new(N_('ISO build failed')) unless system(build_mkiso_command(output_file: iso, extra_commands: mkiso_args, source_directory: File.join(wd, 'build')))

  # Make the ISO bootable as a HDD/USB disk too
  raise Foreman::Exception.new(N_('ISO hybrid conversion failed: %s'), $?) unless system(*["isohybrid", isohybrod_args, iso].flatten.compact)

  yield iso
ensure
  # Clean the working directory (not the ISO file itself)
  FileUtils.rm_f(File.join(wd, 'build'))
  # Temporary directory cannot be cleaned in-process due to asynchronous send_file call.
  # Also we cannot rely on systemd-tmpfiles-clean as private temporary files are not subject
  # of scheduled cleanups. Let's clean bootdisks from prevous requests manually by finding
  # and deleting all directories created 30 minutes ago.
  Rails.root.glob('tmp/bootdisk-iso-*').select { |f| File.ctime(f) < (Time.now.getlocal - 30.minutes) }.each { |f| FileUtils.rm_rf(f) }
end

.generate_full_host(host, opts = {}, &block) ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 15

def self.generate_full_host(host, opts = {}, &block)
  raise Foreman::Exception.new(N_('Host is not in build mode, so the template cannot be rendered')) unless host.build?

  isolinux_template = render_template(host, :PXELinux, ForemanBootdisk::Scope::FullHostBootdisk)
  grub_template = render_template(host, :PXEGrub2, ForemanBootdisk::Scope::FullHostBootdiskEfi)

  # pxe_files and filename conversion is utterly bizarre
  # aim to convert filenames to something usable under ISO 9660, to match as rendered in the template
  # and then still ensure that the fetch() process stores them under the same name
  files = host.operatingsystem.family.constantize::PXEFILES.keys.each_with_object({}) do |type, hash|
    filename = host.operatingsystem.bootfile(host.medium_provider, type)
    iso_filename = iso9660_filename(filename)
    hash[iso_filename] = host.url_for_boot(type)
  end

  generate(opts.merge(isolinux: isolinux_template, grub: grub_template, files: files), &block)
rescue ::Foreman::Exception => e
  if e.code == 'ERF42-0067'
    ForemanBootdisk.logger.warn e
  else
    raise e
  end
end

.iso9660_filename(name) ⇒ Object

isolinux supports up to ISO 9660 level 2 filenames



253
254
255
256
257
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 253

def self.iso9660_filename(name)
  dir  = File.dirname(name)
  file = File.basename(name).upcase.tr_s('^A-Z0-9_', '_').last(28)
  dir == '.' ? file : File.join(dir.upcase.tr_s('^A-Z0-9_', '_').last(28), file)
end

.render_template(host, kind, scope_class) ⇒ Object

Raises:

  • (Foreman::Exception)


39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 39

def self.render_template(host, kind, scope_class)
  template = host.provisioning_template(kind: kind)

  raise Foreman::Exception.new(N_('Unable to generate disk template, %{kind} template not found.'), kind: kind) unless template

  template = ForemanBootdisk::Renderer.new.render_template(
    template: template,
    host: host,
    scope_class: scope_class
  )

  unless template
    err = host.errors.full_messages.to_sentence
    raise Foreman::Exception.new(N_('Unable to generate disk %{kind} template: %{error}'), kind: kind, error: err)
  end

  template
end

.system_or_exception(command, error) ⇒ Object



58
59
60
61
62
63
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 58

def self.system_or_exception(command, error)
  unless system(command)
    ForemanBootdisk.logger.debug "Bootdisk command failed: #{command}"
    raise Foreman::Exception.new(N_(error))
  end
end

.token_expiry(host) ⇒ Object



190
191
192
193
194
195
# File 'app/services/foreman_bootdisk/iso_generator.rb', line 190

def self.token_expiry(host)
  expiry = host.token.try(:expires)
  return '' if Setting[:token_duration].zero? || expiry.blank?

  '_' + expiry.strftime('%Y%m%d_%H%M')
end