#!/usr/bin/ruby
#
# Setup local filesystems for Puavo hosts

require 'fileutils'
require 'getoptlong'
require 'highline/import'
require 'open3'

# XXX this should be elsewhere, other scripts might find this useful
module PuavoFacts
  Puavodir = '/etc/puavo'

  FactPaths = {
		'kerberos_master'   => 'kerberos/master',
		'kerberos_realm'    => 'kerberos/realm',
		'kerberos_toprealm' => 'kerberos/toprealm',
		'ldap_base'         => 'ldap/base',
		'ldap_binddn'       => 'ldap/dn',
		'ldap_bindpw'       => 'ldap/password',
		'ldap_master'       => 'ldap/master',
		'puavo_domain'      => 'domain',
		'puavo_hostname'    => 'hostname',
		'puavo_hosttype'    => 'hosttype',
		'puavo_topdomain'   => 'topdomain',
	      }

  def self.all
    Hash[ FactPaths.map { |name, path| [ name, get(name) ] } ]
  end

  def self.get(name)
    File.read("#{ Puavodir }/#{ FactPaths[ name ] }").chomp \
      or raise "Could not read a fact '#{ name }'"
  end
end

module DiskHandler
  Filesystems = {
    'bootserver' => {
      'swap'          => { 'size' => '16G',      'type' => 'swap' },
      'tmp'           => { 'size' => '16G',      'type' => 'ext4' },
      'images'        => { 'size' => '128G',     'type' => 'ext4' },
      'state'         => { 'size' => '32G',      'type' => 'ext4' },
      'imageoverlays' => { 'size' => '5G',       'type' => 'ext4' },
      'home'          => { 'size' => '100%FREE', 'type' => 'ext4' },
    },
    'diskinstaller' => {
      'installimages' => { 'size' => '100%FREE', 'type' => 'ext4' },
    },
    'laptop' => {
      'swap'          => { 'size' => '4G',       'type' => 'swap' },
      'tmp'           => { 'size' => '6G',       'type' => 'ext4' },
      'images'        => { 'size' => '40G',      'type' => 'ext4' },
      'state'         => { 'size' => '4G',       'type' => 'ext4' },
      'imageoverlays' => { 'size' => '5G',       'type' => 'ext4' },
      'home'          => { 'size' => '100%FREE', 'type' => 'ext4' },
    },
    'wirelessaccesspoint' => {
      'swap'          => { 'size' => '2G',       'type' => 'swap' },
      'tmp'           => { 'size' => '3G',       'type' => 'ext4' },
      'state'         => { 'size' => '2G',       'type' => 'ext4' },
      'imageoverlays' => { 'size' => '128M',     'type' => 'ext4' },
      'images'        => { 'size' => '100%FREE', 'type' => 'ext4' },
    },
  }

  def self.cleanup_disk_environment(hosttype, vgname)
    # Unmount filesystems, if those are mounted.
    non_swap_filesystems(hosttype).each do |name, attrs|
      run('umount', '-f', "/#{ name }", { :err => '/dev/null' }) rescue true
    end

    if hosttype != 'diskinstaller' then
      # Turn off swap partitions from the target device (and possibly some
      # else...).  If our target hosttype is "diskinstaller", we do not need
      # to do this because installer disks should not contain (active) swap
      # partitions (and turning swap off from the current host is not
      # appropriate).  (The really proper solution should disable swap
      # selectively from the disk we are installing to.)
      run('swapoff', '-a', { :err => '/dev/null' }) rescue true
    end

    dm_device_list = \
      case hosttype
        when 'diskinstaller'
          %w(puavoinstaller-installimages)
        else
          %w(swap0) \
            + ((Filesystems[hosttype] || {}) \
                .keys.map { |partname| "puavo-#{ partname }" })
      end

    # remove all dmsetup volumes which may exists on the target host
    dm_device_list.each do |dm_device|
      run('dmsetup', 'remove', '--force', dm_device, { :err => '/dev/null' }) \
        rescue true
    end

    # XXX give some time for "dmsetup remove --force" to have an effect in
    # XXX (frankly: I do not know why this is needed in some situations)
    sleep(2)

    # switch off all volume groups that may interfere with following operations
    run('vgchange', '-a', 'n', vgname,
	{ :close_others => :true, :err => '/dev/null' }) \
      rescue true
  end

  def self.maybe_wipe_current_raid_setups()
    md_device_list \
      = begin
          IO.readlines('/proc/mdstat').map do |line|
            first_field = line.split[0]
            first_field && first_field.match(/\Amd[0-9]+\z/) \
              ? first_field \
              : nil
          end.compact
        rescue Errno::ENOENT
          return
        end

    return unless md_device_list.count > 0

    raise 'System has more than one raid setup, not daring to touch those.' \
      unless md_device_list.count == 1

    md_device_path = "/dev/#{ md_device_list.first }"

    raid_disk_partitions = []
    IO.popen([ 'mdadm', '--detail', md_device_path ]) do |f|
      f.readlines.each do |line|
        fields = line.split
        next unless (0..2).all? do |i|
          fields[i] && fields[i].match(/\A[0-9]+\z/)
        end
        fields.each do |s|
          raid_disk_partitions << s if s.match(/^\/dev/)
        end
      end
    end
    raise 'error running mdadm --detail' unless $?.success?

    # these may not exist and operations may fail, but we must try anyway
    # in case these reserve the raid devices
    system('swapoff', '-a', :err => File::NULL)
    Dir.glob('/dev/puavo/*') do |puavofsdev|
      system('lvremove', '-f', puavofsdev, :err => File::NULL)
    end
    system('vgremove', '-f', 'puavo', :err => File::NULL)

    # these should not fail
    run('mdadm', '--stop', md_device_path)
    raid_disk_partitions.each do |raid_partition|
      run('mdadm', '--zero-superblock', '--force', raid_partition)
    end
    sleep(2)    # give some time for kernel
    run('mdadm', '--remove', md_device_path) if File.exists?(md_device_path)
  end

  def self.setup_raid_if_possible(partitioning_scheme)
    disks_for_mirroring = nil

    loop do
      disks_for_mirroring = QueryDiskInfo::list_disks_for_mirroring()
      break if disks_for_mirroring
      puts "\nYou are doing a bootserver installation but there are no\n" \
              + "two or more identically sized disks available for\n"    \
              + "setting up mirroring raid configuration.\n"
      msg = 'Install without raid mirroring? (type "yes" if this is okay)'
      answer = UI::ask_with_default(msg, 'no', UI::InstallWithoutRaidPrompt)
      return false if answer == 'yes'
    end

    loop do
      puts "\nThe following disks can be used for mirroring RAID:\n"
      QueryDiskInfo::show_disk_devices(disks_for_mirroring)
      msg = "\nUse #{ disks_for_mirroring.join('+') }" \
              + " in mirroring RAID setup?\n" \
              + '(type "yes" to destroy data and setup RAID, "no" otherwise) '

      answer = HighLine.ask(msg) { |q| q.whitespace = nil }
      case answer.strip
        when 'yes'
          break
        when 'no'
          puts 'NOT setting up RAID now.'
          return false
        else
          puts 'Please answer "yes" or "no".'
      end
    end

    maybe_wipe_current_raid_setups()

    disks_for_mirroring.each do |disk|
      raid_partition(disk, partitioning_scheme)
    end

    # gdisk may activate a partial old RAID setup (!?!)
    maybe_wipe_current_raid_setups()

    spare_args = (disks_for_mirroring.count > 2) \
                   ? [ "--spare-devices=#{ disks_for_mirroring.count - 2 }" ] \
                   : []

    run('mdadm', '--create',
                 '--run',
                 '--verbose',
                 '/dev/md0',
                 '--level=mirror',
                 '--raid-devices=2',
                 *spare_args,
                 *(disks_for_mirroring.map do |dev|
                     %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme) \
                       ? "/dev/#{ dev }3" \
                       : "/dev/#{ dev }1"
                   end))

    puts ''

    return true
  end

  def self.setup_partition(device, partitioning_scheme, mbr_format,
                           gpt_format, hybrid_format)
    # clean up possible confusing mess from the device
    run('dd', 'if=/dev/zero', "of=/dev/#{ device }", 'count=2K',
        { :err => '/dev/null' })

    if %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme) then
      # (we also create an EFI partition on a RAID device
      # even though it is pointless in that case)
      IO.popen([ 'gdisk', "/dev/#{ device }"], 'w') do |f|
        f.print(gpt_format)
      end
    else
      # create an lvm partition
      # see http://www.walkernews.net/2007/07/02/how-to-create-linux-lvm-in-3-minutes/
      IO.popen([ 'fdisk', "/dev/#{ device }" ], 'w') do |f|
        f.print(mbr_format)
      end
    end

    # when creating a disk image file (with puavo-make-install-disk)
    # we need to do this, otherwise this appears to be useless
    # Edit: this seems to be the case for all creations of vfat partitions
    # with # higher than the highest in the previous partition table.
    # e.g. the typical usb stick with 1 partition fails when creating
    # uefi booting stick, since two vfat partitions are to be created.
    # this might also be the case in some installations; needs more
    # inspecting however, as the error couldn't be reproduced at least
    # with completely the same steps as with the usb sticks
    wait_iterations = 0
    partition_dev = nil
    if UI::loopback_only then
      partition_dev = "/dev/#{ device }p1"
    elsif UI::hosttype == 'diskinstaller' && %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme)
      partition_dev = "/dev/#{ device }2"
    end
    if partition_dev
      until File.exists?(partition_dev) do
        run('partprobe', "/dev/#{ device }")
        sleep(1)
        wait_iterations += 1
        raise "could not find #{ partition_dev }" if wait_iterations > 30
      end
    end

    if %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme) then
      create_efi_filesystem(device)
    end

    # hybrid partitioning apparently needs to be set up after normal partitioning,
    # as is done here. one could think this is strange, running gdisk twice in this
    # function, but everything is ok.
    if partitioning_scheme == 'UEFI/MBR+GPT' then
      IO.popen([ 'gdisk', "/dev/#{ device }"], 'w') do |f|
        f.print(hybrid_format)
      end
    end
  end

  def self.raid_partition(device, partitioning_scheme)
    setup_partition(device, partitioning_scheme,
                    "n\np\n1\n\n\nt\nfd\na\np\nw\n",
                    "2\no\ny\nn\n1\n\n+1M\nef02\nn\n2\n\n+512M\nef00\nn\n3\n\n\nfd00\nw\ny\n",
                    "r\nh\n1 2 3\nn\n\ny\n\nn\n\nn\nw\ny\n")
  end

  def self.get_partition(device, partitioning_scheme, numstart)
    special_disk_regexp = %r{\A((loop|md|mmcblk)[0-9]+|nvme[0-9]+n[0-9]+)\z}
    part_prefix = device.match(special_disk_regexp) ? 'p' : ''
    puavo_partnum = %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme) \
                       ? (numstart + 2) \
                       : numstart
    [ device, part_prefix, puavo_partnum ].join
  end

  def self.get_puavo_partition(device, partitioning_scheme)
    get_partition(device, partitioning_scheme, 1)
  end

  def self.do_fs_setup(conf, vgname, partitioning_scheme)
    fs_conf = conf.clone

    fs_setup_phases = []
    if fs_conf['partition'] == 'whole' then
      fs_setup_phases += [ :wipe_device ] if conf['wipe']
      fs_conf['partition'] = get_puavo_partition(fs_conf['device'],
                                                 partitioning_scheme)
      fs_setup_phases += [ :create_partitions, :puavo_filesystems ]
    else
      fs_setup_phases += [ :wipe_partition ] if conf['wipe']
      fs_setup_phases += [ :puavo_filesystems ]
    end

    fs_setup_phases += [ :mount_filesystems ]

    fs_setup_phases.each do |fn_sym|
      args = [ :cleanup_disk_environment,
               :mount_filesystems,
               :puavo_filesystems ].include?(fn_sym) ? [ fs_conf, vgname ] : \
             :create_partitions == fn_sym                                \
                ? [ fs_conf, partitioning_scheme ]                       \
                : [ fs_conf ]
      method(fn_sym).call(*args)
    end
  end

  def self.run(*cmd)
    system(*cmd) or raise "Error running command: '#{ cmd.inspect }'"
  end

  def self.create_partitions(fs_conf, partitioning_scheme)
    setup_partition(fs_conf['device'], partitioning_scheme,
                    "n\np\n1\n#{ fs_conf['first_sector'] }\n\nt\n8e\na\np\nw\n",
                    "2\no\ny\nn\n1\n\n+1M\nef02\nn\n2\n\n+512M\nef00\nn\n3\n\n\n8e00\nw\ny\n",
                    "r\nh\n1 2 3\nn\n\ny\n\nn\n\nn\nw\ny\n")
  end

  def self.non_swap_filesystems(hosttype)
    Filesystems[ hosttype ] \
      .select { |name, attrs| attrs['type'] != 'swap' }
  end

  def self.create_efi_filesystem(device)
    efi_partition = get_partition(device, 'UEFI/GPT', 0)
    run('mkfs.fat', '-F32', "/dev/#{ efi_partition }")
  end

  def self.puavo_filesystems(fs_conf, vgname)
    # this is needed again, because gdisk may activate kernel with the
    # new partitions, with lvm activated as well (probably only in the case
    # where the partitioning is the same)
    cleanup_disk_environment(fs_conf['hosttype'], vgname)

    # clean up possible confusing mess from the partition
    run('dd', 'if=/dev/zero', "of=/dev/#{ fs_conf['partition'] }", 'count=2K',
       { :err => '/dev/null' })

    # create LVM volume groups
    run('pvcreate', "/dev/#{ fs_conf['partition'] }", { :close_others => true })
    run('vgcreate', '-s', '64M', vgname, "/dev/#{ fs_conf['partition'] }",
	{ :close_others => true })

    Filesystems[ fs_conf['hosttype'] ].each do |name, attrs|
      mkfs(attrs['size'], name, attrs['type'], vgname)
    end
  end

  def self.mkfs(size, name, type, vgname)
    run('lvcreate',
	(size.match(/%/) ? '--extents' : '--size'), size,
	'--name', name,
	'--yes',
	vgname,
	{ :close_others => true })

    case type
      when 'ext4'
        run('mkfs.ext4', "/dev/mapper/#{ vgname }-#{ name }")
      when 'swap'
        run('mkswap', '-f', "/dev/mapper/#{ vgname }-#{ name }")
    end
  end

  def self.mount_filesystems(fs_conf, vgname)
    non_swap_filesystems( fs_conf['hosttype'] ).each do |name, attrs|
      # /tmp should not be mounted now,
      # otherwise things currently using it will break (for example tmux).
      next if name == 'tmp'

      mnt_path = "/#{ name }"
      FileUtils.mkdir_p(mnt_path)
      run('mount', "/dev/mapper/#{ vgname }-#{ name }", mnt_path)
    end
  end

  def self.wipe(device)
    run('nwipe', '--autonuke',
                 '--method=quick',
                 '--nowait',
                 '--verify=off',
                 "/dev/#{ device }")
  end

  def self.wipe_device(fs_conf)   ; wipe(fs_conf['device'])   ; end
  def self.wipe_partition(fs_conf); wipe(fs_conf['partition']); end
end

module QueryDiskInfo
  def self.disk_device_regexp()
    if UI::loopback_only then
      return /\Aloop[0-9]+\z/
    else
      # possible choices here should sort so that "md0" is the first if it exists
      return /\A((md|mmcblk)[0-9]+|nvme[0-9]+n[0-9]+|[sv]d[a-z]|xvd[a-z])\z/
    end
  end

  def self.list_disks_for_mirroring()
    # return a list of devices if there are multiple disks with the same size,
    # otherwise return nil

    devices_by_size = {}
    IO.readlines('/proc/partitions').each do |line|
      disk_size, disk_name = * line.split(' ')[2,3]
      next unless disk_size && disk_name
      next unless disk_name.match(disk_device_regexp)
      next if disk_name.match(/\Amd[0-9]+\z/)       # no raid devices
      (devices_by_size[disk_size.to_i] ||= []) << "#{ disk_name }"
    end

    duplicate_sized_disks = devices_by_size.select do |size, disks|
                              disks.count > 1
                            end
    return nil if duplicate_sized_disks.empty?
    biggest_common_disksize = duplicate_sized_disks.keys.sort.last
    return devices_by_size[biggest_common_disksize].sort
  end

  def self.ask_device()
    choosable_disk_devices = []

    current_system_device = nil
    begin
      current_system_device = get_current_system_device()
    rescue StandardError => e
      msg = "Could not find the current system device: #{ e }\n"        \
            + "It may appear on the list of devices to install to,\n"   \
            + "but the install to it should fail (maybe it will not?)."
      STDERR.puts msg
    end

    chosen_disk_device = nil
    until choosable_disk_devices.include?(chosen_disk_device) do
      # these should sort here so that "md0" is the first if it exists
      all_disk_devices = IO.readlines('/proc/partitions') \
                           .map { |s| s.split(' ')[3] }   \
                           .select { |s| s && s.match(disk_device_regexp) } \
                           .sort

      # do not allow installation to the device where the currently running
      # system resides
      choosable_disk_devices \
        = all_disk_devices - [ current_system_device ].compact

      if choosable_disk_devices.empty? then
        raise 'Could not find any disk device where we could install to'
      end

      puts '-----'
      puts "We can install to the following disk devices:\n"
      show_disk_devices(choosable_disk_devices)

      if chosen_disk_device.nil? then
        chosen_disk_device = choosable_disk_devices[0]
      end

      puts "\nPossible disk devices are: #{ choosable_disk_devices.join(' ') }"
      chosen_disk_device = UI::ask_with_default('Choose a disk device:',
                                                chosen_disk_device,
                                                UI::DiskDevicePrompt)
    end

    chosen_disk_device
  end

  def self.ask_confirmation(device, partition)
    if device.nil? || device.empty? || partition.nil? || partition.empty?
      return false
    end

    puts %Q{\nYou are going to install to device "/dev/#{ device }", } \
	 + %Q{partition "#{ partition }".}
    UI::colormsg("IF YOU PROCEED, ALL DATA ON THAT PARTITION IS DESTROYED!\n",
		 HighLine::RED)

    prompt = %q{Are you sure you want to proceed (write "yes" if this is okay?)}

    UI::ask_with_default(prompt, 'no', UI::WritePartitionsPrompt) == 'yes'
  end

  def self.ask_device_and_partition_with_confirmation(hosttype,
             raid_setup_done, partitioning_scheme)
    do_it        = false
    device       = nil
    partition    = nil
    first_sector = nil
    wipe         = false

    until do_it do
      UI::reset_questions()

      if raid_setup_done then
        device    = 'md0'
        partition = 'whole'
      else
        device    = QueryDiskInfo::ask_device()
        partition = QueryDiskInfo::ask_partition(device, partitioning_scheme)
      end

      if partition == 'whole'
        first_sector = 2048

        # Let the user set the size of the following filesystems:
        [
          ['imageoverlays', UI::ImageoverlaySizePrompt],
        ].each do |fs_name, prompt_key|
          fs = DiskHandler::Filesystems[hosttype][fs_name]
          next if fs.nil?
          fs_size =
            UI::ask_size_with_default("Filesystem size of #{ fs_name }:",
                                      fs['size'], prompt_key)

          fs['size'] = fs_size.join()
        end
      end

      msg = "\nIt is possible to wipe disk/partition before installing,\n" \
              + "but this can take a rather long time.\n"                  \
              + "Should we wipe the disk/partition before installing? (yes/no)"
      while true do
        wipe_answer = UI::ask_with_default(msg, 'no', UI::WipePartitionPrompt)
        case wipe_answer
          when 'yes'
            wipe = true
            break
          when 'no'
            wipe = false
            break
        end
      end

      do_it = QueryDiskInfo::ask_confirmation(device, partition)
    end

    {
      'device'       => device,
      'first_sector' => first_sector,
      'hosttype'     => hosttype,
      'partition'    => partition,
      'wipe'         => wipe,
    }
  end

  def self.ask_partition(disk_device, partitioning_scheme)
    partitions = []

    chosen_partition = 'whole'
    until partitions.include?(chosen_partition) do
      if chosen_partition == 'fdisk' then
        system('fdisk', "/dev/#{ disk_device }")
        chosen_partition = 'whole'
      elsif chosen_partition == 'gdisk' then
        system('gdisk', "/dev/#{ disk_device }")
        chosen_partition = 'whole'
      end

      puts '-----'
      puts "The disk /dev/#{ disk_device } has the following partitions:\n"
      show_disk_devices([ disk_device ])

      partition_regexp = /\A#{ Regexp.quote(disk_device) }p?[0-9]+\z/

      partitions \
        = %w(whole) \
          + IO.readlines('/proc/partitions') \
              .map { |line| (line.split)[3] } \
              .compact \
              .select { |part| part.match(partition_regexp) }

      partedit_program = \
        %w(UEFI/GPT UEFI/MBR+GPT).include?(partitioning_scheme) \
          ? 'gdisk' : 'fdisk'

      puts "\nPossible partitions are: #{ partitions.join(' ') }"
      puts "\nYou may choose '#{ partedit_program }' to partition this disk,"
      puts "or choose 'whole' to use the whole disk.\n"
      chosen_partition = UI::ask_with_default('Choose a partition:',
                                              chosen_partition,
                                              UI::PartitionPrompt)
    end
    chosen_partition
  end

  def self.get_current_system_device
    rootdev = nil
    File.read('/proc/cmdline').split.each do |kernel_arg|
      match = kernel_arg.match(/\Aroot=(.*)\z/)
      if match then
        rootdev = match[1]
        break
      end
    end

    raise 'Could not find root= argument in kernel command line' \
      unless rootdev

    match = rootdev.match(%r{\A/dev/(nbd.*)\z})
    return match[1] if match

    match = rootdev.match(%r{\A/dev/mapper/(.*)\z})
    raise 'Did not understand the root= argument in kernel command line' \
      unless match
    root_dm_name = match[1]

    images_dm_path = nil
    Dir.glob('/sys/block/dm-*').each do |dm_name_path|
      if File.read("#{ dm_name_path }/dm/name").chomp == root_dm_name then
        images_dm_path = dm_name_path
        break
      end
    end
    raise 'Could not find the logical volume where the images are' \
      unless images_dm_path

    Dir.glob("#{ images_dm_path }/slaves/*") do |dm_slave|
      return File.basename( File.dirname( File.readlink(dm_slave) ) )
    end
    raise 'Could not find the device where the images are' \
      unless images_dm_path
  end

  def self.show_disk_devices(disk_devices)
    sleep(1.5)

    disk_devices.each do |device|
      print "\n[#{ device }]\n"
      system("fdisk -l /dev/#{ device } | sed 's/^/  /' | more -e") \
        or raise "Error running fdisk for device '#{ device }'"
    end
  end
end

module Size
  UNIT_BYTES = {
    'K' => 1024 ** 1,
    'M' => 1024 ** 2,
    'G' => 1024 ** 3,
  }

  def self.parse(str)
    unit = str[-1]
    if not UNIT_BYTES.include?(unit)
      raise ArgumentError, "invalid unit, expected one from #{UNIT_BYTES.keys}"
    end

    number = str[0..-2]
    value = Integer(number)
    if value < 0
      raise ArgumentError, "invalid size, expected a non-negative value"
    end

    [value, unit]
  end

  def self.to_bytes(size)
    value, unit = size
    value * UNIT_BYTES[unit]
  end

end

module UI
  DiskDevicePrompt,
  ImageoverlaySizePrompt,
  InstallWithoutRaidPrompt,
  PartitionPrompt,
  UnpartitionedSpacePrompt,
  WipePartitionPrompt,
  WritePartitionsPrompt,
    = *(1..7)

  @ask_forced      = {}
  @defaults        = {}
  @force_defaults  = {}
  @hosttype        = nil
  @loopback_only   = false
  @partition_label = nil

  def self.hosttype       ; @hosttype        end
  def self.loopback_only  ; @loopback_only   end
  def self.partition_label; @partition_label end

  def self.reset_questions
    @ask_forced = @force_defaults.clone
  end

  def self.help_and_exit
    puts <<-EOF
puavo-setup-filesystems [OPTIONS]

-h, --help                        show help

    --ask-disk-device             ask with default for disk device
    --ask-imageoverlay-size       ask with default for imageoverlay size
    --ask-install-without-raid    ask with default for install without raid
    --ask-partition               ask with default for partition
    --ask-unpartitioned-space     ask with default for unpartitioned space
    --ask-wipe-partition          ask with default for wipe partition
    --ask-write-partitions        ask with default for write partitions
    --force-disk-device           force value for disk device
    --force-imageoverlay-size     force value for imageoverlay size
    --force-install-without-raid  force value for install without raid
    --force-partition             force value for partition
    --force-unpartitioned-space   force value for unpartitioned space
    --force-wipe-partition        force value for wipe partition
    --force-write-partitions      force value for write partitions

    --loopback-only               use only loopback devices
    --partition-label             use partition label "BIOS/MBR", "UEFI/GPT"
                                    or "UEFI/MBR+GPT"

EOF
  end

  def self.parse_args
    begin
      opts = GetoptLong.new(
        [ '--help', '-h'                , GetoptLong::NO_ARGUMENT       ],
        [ '--ask-disk-device'           , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-imageoverlay-size'     , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-install-without-raid'  , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-partition'             , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-unpartitioned-space'   , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-wipe-partition'        , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--ask-write-partitions'      , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-disk-device'         , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-imageoverlay-size'   , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-install-without-raid', GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-partition'           , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-unpartitioned-space' , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-wipe-partition'      , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--force-write-partitions'    , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--hosttype'                  , GetoptLong::REQUIRED_ARGUMENT ],
        [ '--loopback-only'             , GetoptLong::NO_ARGUMENT       ],
        [ '--partition-label'           , GetoptLong::REQUIRED_ARGUMENT ],
      )

      opts.each do |opt, arg|
        case opt
        when '--help'
          help_and_exit
          exit(0)
        when '--ask-disk-device'
          @defaults[DiskDevicePrompt] = arg
        when '--ask-imageoverlay-size'
          @defaults[ImageoverlaySizePrompt] = arg
        when '--ask-install-without-raid'
          @defaults[InstallWithoutRaidPrompt] = arg
        when '--ask-partition'
          @defaults[PartitionPrompt] = arg
        when '--ask-unpartitioned-space'
          @defaults[UnpartitionedSpacePrompt] = arg
        when '--ask-wipe-partition'
          @defaults[WipePartitionPrompt] = arg
        when '--ask-write-partitions'
          @defaults[WritePartitionsPrompt] = arg
        when '--force-disk-device'
          @defaults[DiskDevicePrompt] = arg
          @force_defaults[DiskDevicePrompt] = true
        when '--force-imageoverlay-size'
          @defaults[ImageoverlaySizePrompt] = arg
          @force_defaults[ImageoverlaySizePrompt] = true
        when '--force-install-without-raid'
          @defaults[InstallWithoutRaidPrompt] = arg
          @force_defaults[InstallWithoutRaidPrompt] = true
        when '--force-partition'
          @defaults[PartitionPrompt] = arg
          @force_defaults[PartitionPrompt] = true
        when '--force-unpartitioned-space'
          @defaults[UnpartitionedSpacePrompt] = arg
          @force_defaults[UnpartitionedSpacePrompt] = true
        when '--force-wipe-partition'
          @defaults[WipePartitionPrompt] = arg
          @force_defaults[WipePartitionPrompt] = true
        when '--force-write-partitions'
          @defaults[WritePartitionsPrompt] = arg
          @force_defaults[WritePartitionsPrompt] = true
        when '--hosttype'
          @hosttype = arg
        when '--loopback-only'
          @loopback_only = true
        when '--partition-label'
          case arg
            when 'BIOS/MBR', 'UEFI/GPT', 'UEFI/MBR+GPT'
              @partition_label = arg
            else
              warn "Unsupported partition label #{ arg }"
              help_and_exit
              exit(1)
          end
        end
      end
    rescue GetoptLong::InvalidOption => e
      help_and_exit
      exit(1)
    end
  end

  def self.ask_with_default(question, default, prompt_key=nil)
    if @defaults.has_key?(prompt_key) && @defaults[prompt_key] != 'default' then
      default = @defaults[prompt_key]
    end
    if @ask_forced.has_key?(prompt_key) then
      # Do not return forced twice... in case it did not work,
      # we need user input.
      @ask_forced.delete(prompt_key)
      return default
    end
    prompt = "#{ question } [#{ default }] "
    answer = HighLine.ask(prompt) { |q| q.whitespace = nil }

    return answer.match(/\A\n\z/)  ? default \
         : answer.match(/\A\s+\z/) ? ''      \
         : answer.strip
  end

  def self.ask_size_with_default(question, default, prompt_key=nil)
    while true do
      str = ask_with_default(question, default, prompt_key)
      begin
        size = Size::parse(str)
      rescue ArgumentError => e
        colormsg(e.message, HighLine::RED)
        next
      end
      break
    end
    size
  end

  def self.colormsg(msg, color)
    HighLine.say(HighLine.new.color(msg, color))
  end
end

UI::parse_args

hosttype = UI::hosttype || PuavoFacts::get('puavo_hosttype')
unless DiskHandler::Filesystems[hosttype] then
  warn "No filesystem scheme defined for hosttype '#{ hosttype }'," \
         + ' try --hosttype'
  exit(1)
end

vgname = (hosttype == 'diskinstaller') ? 'puavoinstaller' : 'puavo'

DiskHandler::cleanup_disk_environment(hosttype, vgname)

partitioning_scheme = UI::partition_label \
  || (Dir.exists?('/sys/firmware/efi') ? 'UEFI/GPT' : 'BIOS/MBR')

raid_setup_done = false
if hosttype == 'bootserver' then
  raid_setup_done = DiskHandler::setup_raid_if_possible(partitioning_scheme)
end

case hosttype
  when 'bootserver', 'diskinstaller', 'laptop', 'wirelessaccesspoint'
    conf = QueryDiskInfo::ask_device_and_partition_with_confirmation(hosttype,
             raid_setup_done, partitioning_scheme)
    DiskHandler::do_fs_setup(conf, vgname, partitioning_scheme)
  else
    puts "Hosttype '#{ hosttype }' does not need to setup local filesystems."
end
