#!/usr/bin/ruby

# Copyright (C) 2018 Thorsten Kukuk
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# in Version 2 as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

require 'optparse'
require 'fileutils'
require 'tempfile'
require "suse/connect"
require "inifile"

def release_notes_url(product, version)
  myproduct = product
  myversion = version
  case myproduct
  when "SLES", "SLED"
    myversion.sub! '.','-SP'
    myproduct = "SUSE-#{product}"
  when "SLE-HPC"
    myversion.sub! '.','-SP'
  when "CAASP"
    myproduct = "SUSE-#{product}"
  end
  url = "https://www.suse.com/releasenotes/x86_64/#{myproduct}/#{myversion}/"
  return url
end

def update_grains(available, mirrored, newversion, url)
  filename = "/etc/salt/grains"
  available_found = 0;
  mirrored_found = 0;
  newversion_found = 0;
  notes_found = 0;
  modified = 0;

  Tempfile.open(".#{File.basename(filename)}", File.dirname(filename)) do |tempfile|
    if File.exist?(filename)
      File.open(filename).each do |line|
        if line =~ /^tx_update_migration_available/
          line = "tx_update_migration_available: #{available}\n"
          available_found=1
          modified = 1;
        end
        if line =~ /^tx_update_migration_mirror_synced/
          line = "tx_update_migration_mirror_synced: #{mirrored}\n"
          mirrored_found=1
          modified = 1;
        end
        if line =~ /^tx_update_migration_newversion/
          line = "tx_update_migration_newversion: \"#{newversion}\"\n"
          newversion_found=1
          modified = 1;
        end
        if line =~ /^tx_update_migration_notes/
          line = "tx_update_migration_notes: \"#{url}\"\n"
          notes_found=1
          modified = 1;
        end
        tempfile.puts line
      end
    end
    if available_found == 0
      tempfile.puts "tx_update_migration_available: #{available}\n"
      modified = 1;
    end
    if mirrored_found == 0 && available
      tempfile.puts "tx_update_migration_mirror_synced: #{mirrored}\n"
      modified = 1;
    end
    if newversion_found == 0 && available
      tempfile.puts "tx_update_migration_newversion: \"#{newversion}\"\n"
      modified = 1;
    end
    if notes_found == 0 && available
      tempfile.puts "tx_update_migration_notes: \"#{url}\"\n"
      modified = 1;
    end
    tempfile.fdatasync
    tempfile.close
    if modified > 0
      if File.exist?(filename)
        stat = File.stat(filename)
        FileUtils.chown stat.uid, stat.gid, tempfile.path
        FileUtils.chmod stat.mode, tempfile.path
      else
        FileUtils.chown 'root', 'root', tempfile.path
        FileUtils.chmod 0644, tempfile.path
      end
      FileUtils.mv tempfile.path, filename
    else
      File.delete("#{File.dirname(filename)}/.#{File.basename(filename)}")
    end
  end
end


options = {
  :config => "/etc/update-checker.conf",
  :profile => nil,
  :output => nil,
  :verbose => false,
  :root => nil
}

STDOUT.sync = true

save_argv = Array.new(ARGV)

OptionParser.new do |opts|
  opts.banner = "Usage: update-checker-migration [options]"

  opts.on("-c", "--config FILE", "Specify an alternate config file") do |f|
    options[:config] = f
  end

  opts.on("-o", "--output string", "Specify backend for output") do |o|
    options[:output] = o
  end

  opts.on("-p", "--profile string", "Use entry from different profile from the configuration file") do |p|
    options[:profile] = p
  end

  opts.on("-v", "--[no-]verbose", "Increase verbosity") do |v|
    options[:verbose] = v
  end

  opts.on("-R", "--root DIR", "Operate on a different root directory") do |r|
    options[:root] = r
    SUSE::Connect::System.filesystem_root = r
  end

end.parse!

# Set output variable
output_stdout=false
output_issue=false
# for salt we assume, that there is at max. one migration target.
# Else we try to find the one with the highest version number.
output_salt=false
tx_update_migration_available=false
tx_update_migration_mirror_synced=true
tx_update_migration_newversion=nil
tx_update_migration_notes=nil

# Read config file if necessary
if options[:output]
  # don't read output from config file, use specified one
  argument=options[:output].delete(" \t\r\n")
  argument.split(',').each do |arg|
    case arg
    when "stdout"
      output_stdout=true
    when "issue"
      output_issue=true
    when "salt"
      if !File.directory?("/etc/salt")
        $stderr.puts "ERROR: no salt installed! Ignoring salt output.\n"
      else
        output_salt=true
      end
    else
      $stderr.puts "ERROR: unknown output value: #{arg}\n"
    end
  end
else
  begin
    # read output variable from config file
    config_file = IniFile.load(options[:config])
    if config_file
      if options[:profile]
        profile="global-#{options[:profile]}"
        if !config_file.has_section?(profile)
          profile="global"
        end
      else
        profile="global"
      end
      data = config_file[profile]
      argument = data["output"]
      if argument
        argument = argument.delete(" \t\r\n")
        argument.split(',').each do |arg|
          case arg
          when "stdout"
            output_stdout=true
          when "issue"
            output_issue=true
          when "salt"
            if !File.directory?("/etc/salt")
              $stderr.puts "ERROR: no salt installed! Ignoring salt output.\n"
            else
              output_salt=true
            end
          else
            $stderr.puts "ERROR: unknown output value: #{arg}\n"
          end
        end
      end
    end
  end
end

if output_stdout == false && output_issue == false && output_salt == false
  output_stdout=true
end


if !File.file?((options[:root] ? "#{options[:root]}" : "") + "/etc/zypp/credentials.d/SCCcredentials")
  print "System not registered\n"
  exit 0
end

# Check if we have migration targets
begin
  system_products = SUSE::Connect::Migration::system_products

  if system_products.length == 0
    $stderr.puts "No products found\n"
    exit 1
  end

  if options[:verbose]
    print "Installed products:\n"
    system_products.each {|p|
      printf "  %-25s %s\n", "#{p.identifier}/#{p.version}/#{p.arch}", p.summary
    }
    print "\n"
  end
rescue => e
  $stderr.puts "Can't determine the list of installed products: #{e.class}: #{e.message}\n"
  exit 1
end

# Fetch migration targets
begin
  migrations_all = SUSE::Connect::YaST.system_migrations system_products
rescue => e
  $stderr.puts "Can't get available migrations from server: #{e.class}: #{e.message}\n"
  exit 1
end

if migrations_all.length == 0
  print "No migration targets found\n" if options[:verbose]
  exit 0
end

if options[:to_product]
  begin
    identifier, version, arch = options[:to_product].split('/')
    new_product = OpenStruct.new(
      identifier: identifier,
      version:   version,
      arch:       arch
    )
    migrations_all = SUSE::Connect::YaST.system_offline_migrations(system_products, new_product)
  rescue => e
    $stderr.puts "Can't get available migrations from server: #{e.class}: #{e.message}\n"
    exit 1
  end
end

#preprocess the migrations lists
migrations = Array.new
migrations_unavailable = Array.new
migrations_all.each do |migration|
  migr_available = true
  migration.each do |p|
    p.available = !defined?(p.available) || p.available
    p.already_installed = !! system_products.detect { |ip| ip.identifier.eql?(p.identifier) && ip.version.eql?(p.version) && ip.arch.eql?(p.arch) }
    if !p.available
      migr_available = false
    end
  end
  # put already_installed products last and base products first
  migration = migration.sort_by.with_index { |p, idx| [p.already_installed ? 1 : 0, p.base ? 0 : 1, idx] }
  if migr_available
    migrations << migration
  else
    migrations_unavailable << migration
  end
end

if migrations_unavailable.length > 0
  print "Unavailable migrations (product is not mirrored):\n\n" if options[:verbose];
  migrations_unavailable.each do |migration|
    url=""
    product_name=""
    migration.each do |p|
      print " #{p.identifier}/#{p.version}/#{p.arch}" + (p.available ? "" : " (not available)") + (p.already_installed ? " (already installed)" : "") + "\n       " if options[:verbose]
      if p.base
        url = release_notes_url(p.identifier, p.version)
        product_name=p.friendly_name
      end
    end
    print "#{url}\n\n" if options[:verbose]
    if output_salt && (tx_update_migration_available == false || (product_name <=> tx_update_migration_newversion) > 0)
      tx_update_migration_available=true
      tx_update_migration_mirror_synced=false
      tx_update_migration_newversion=product_name
      tx_update_migration_notes=url
    end
  end
  print "\n" if options[:verbose]
end

if migrations.length > 0
  print "Available migrations:\n\n" if output_stdout
  number=0
  migrations.each do |migration|
    number+=1
    url=""
    product_list=""
    product_name=""
    migration.each do |p|
      if p.base
        url = release_notes_url(p.identifier, p.version)
        product_name = p.friendly_name
      end
      if output_stdout
        print " #{p.identifier}/#{p.version}/#{p.arch}" + (p.already_installed ? " (already installed)" : "") + "\n       "
      end
      if output_issue && !p.already_installed && number == 1
        product_list=product_list + " #{p.identifier}/#{p.version}/#{p.arch}"
      end
    end
    if output_stdout
      print "#{url}\n"
      print "\n"
    end
    if output_issue && number == 1
      begin
        file = File.open("/var/run/issue.d/82-migration", "w")
      rescue IOError => e
        $stderr.puts "ERROR: creating of /var/run/issue.d/82-migration failed: #{e.class}: #{e.message}\n"
      end
      begin
        file.write("New version available:#{product_list}\nRelease Notes: #{url}\n\n")
      rescue IOError => e
        $stderr.puts "ERROR: writing of /var/run/issue.d/82-migration failed: #{e.class}: #{e.message}\n"
      ensure
        file.close unless file.nil?
      end
      if ! system "/usr/sbin/issue-generator"
        $stderr.puts "Executing /usr/sbin/issue-generator failed!\n\n"
      end
    end
    if output_salt && (tx_update_migration_available == false || (product_name <=> tx_update_migration_newversion) > 0)
      tx_update_migration_available=true
      tx_update_migration_mirror_synced=true
      tx_update_migration_newversion=product_name
      tx_update_migration_notes=url
    end
  end
  print "\n" if output_stdout
end

if (output_salt && tx_update_migration_available)
  if options[:verbose]
    print "tx_update_migration_available: #{tx_update_migration_available}\n"
    print "tx_update_migration_mirror_synced: #{tx_update_migration_mirror_synced}\n"
    print "tx_update_migration_newversion: #{tx_update_migration_newversion}\n"
    print "tx_update_migration_notes: #{tx_update_migration_notes}\n"
  end
  update_grains(tx_update_migration_available, tx_update_migration_mirror_synced, tx_update_migration_newversion,
                tx_update_migration_notes)
end

exit 0
