#!/usr/bin/ruby

require 'fileutils'
require 'json'
require 'puavo/conf'
require 'puavo/rest-client'
require 'syslog'

def syslog(channel, priority, *args)
  Syslog.log(priority, *args)
  channel.printf(*args)
end

def log(*args)   ; syslog(STDOUT, *args); end
def logerr(*args); syslog(STDERR, *args); end

def get_hostname()
  # Hostname must be read from here, and not from Socket.gethostname or some
  # such, because this should also work when host in still name "unregistered"
  # at install phase (another alternative would be to set hostname at install
  # before this script is run).
  IO.read('/etc/puavo/hostname').chomp
end

def puavoconf_get(key)
  # Open and close puavo-conf db each time, so that we will not be holding
  # a lock when "puavo-conf-update" gets run, as it also wants a db access
  # and a lock (or worse, it starts up puavo-conf-daemon which does not get
  # a lock).
  puavoconf = Puavo::Conf.new
  begin
    return puavoconf.get(key)
  ensure
    puavoconf.close
  end
end

def puavo_rest_request_and_replace(restclient, uripath, filepath)
  data = restclient.get(uripath).to_s

  tmp_filepath = "#{ filepath }.tmp"
  File.open(tmp_filepath, 'w', 0600) { |f| f.write(data) }

  replace_if_changed(filepath, tmp_filepath)
end

def replace_if_changed(dest, src)
  if (FileUtils.cmp(dest, src) rescue false) then
    FileUtils.rm_f(src)
  else
    File.rename(src, dest)
  end
end

def run(*args)
  system(*args) or raise "Running system command #{ args } returned failure"
end

def send_changed_primary_user_to_puavo(primary_user)
  log(Syslog::LOG_NOTICE,
      "Sending overridden primary user %s to Puavo\n",
      primary_user ? primary_user : '(none)')

  cloud_restclient = PuavoRestClient.new(:auth    => :etc,
                                         :dns     => :no,
                                         :timeout => 30)

  senddata = { 'primary_user' => primary_user }

  uripath = "/v3/devices/#{ get_hostname() }"
  cloud_restclient.post(uripath, :json => senddata)
end

def do_nothing(restclient); end

def manage_local_puavoconf(conf)
  local_puavoconf_path = '/state/etc/puavo/local/puavo_conf.json'
  begin
    local_puavoconf = JSON.parse( IO.read(local_puavoconf_path) )
  rescue Errno::ENOENT
    return
  end

  enableconfmap = {
    'puavo.laptop-setup.enable_setting.abitti_version' \
      => 'puavo.abitti.version',
    'puavo.laptop-setup.enable_setting.boot_default' \
      => 'puavo.grub.boot_default',
    'puavo.laptop-setup.enable_setting.windows_enabled' \
      => 'puavo.grub.windows.enabled',
  }
  enableconfmap.each do |enable_key, target_key|
    enable_value = conf[enable_key] || puavoconf_get(enable_key)
    next unless enable_value == 'false'
    next unless local_puavoconf[target_key]
    log(Syslog::LOG_INFO,
        "Resetting local puavo-conf variable #{ target_key }\n")
    run('puavo-conf-local', '-u', target_key)
  end

  removekeys = [ 'puavo.pkg.adobe-flashplugin',
                 'puavo.pkg.adobe-pepperflashplugin' ]
  removekeys.each do |target_key|
    next unless local_puavoconf[target_key]
    log(Syslog::LOG_INFO,
        "Resetting local puavo-conf variable #{ target_key }\n")
    # XXX this unsets only, could just as well remove
    run('puavo-conf-local', '-u', target_key)
  end
end

def update_device_json(restclient)
  log(Syslog::LOG_INFO, ">>> Updating device json\n")

  # We override the primary_user information in device.json from
  # /state/etc/puavo/primary_user_override if that exists.

  device_json_path       = '/etc/puavo/device.json'
  state_device_json_path = "/state#{ device_json_path }"

  uripath = ($puavo_hosttype == 'bootserver' \
	       ? "v3/boot_servers/#{ get_hostname() }" \
	       : "v3/devices/#{ get_hostname() }")

  device = nil
  begin
    device = restclient.get(uripath).parse()
  rescue PuavoRestClient::BadStatusCode => e
    if e.response.code == 404 || e.response.code == 401 then
      FileUtils.touch('/state/etc/puavo/HOST_REMOVED_FROM_PUAVO')
    end
    raise e
  end
  FileUtils.rm_f('/state/etc/puavo/HOST_REMOVED_FROM_PUAVO')

  remove_primary_user_override = false

  primary_user_override \
    = IO.read('/state/etc/puavo/primary_user_override').chomp rescue nil

  if primary_user_override then
    # If primary_user_override is set but an empty string,
    # then we should unset it in Puavo (set to nil).
    primary_user_override = nil if primary_user_override.empty?
    if primary_user_override == device['primary_user'] then
      # Remove override if we get the same information from Puavo
      # (we are sure to check against the info just received from Puavo,
      # not puavo-conf or any other local db).
      remove_primary_user_override = true
    else
      begin
        send_changed_primary_user_to_puavo(primary_user_override)
      rescue PuavoRestClient::BadStatusCode => e
        logerr(Syslog::LOG_ERR,
               "Bad status code when sending primary user to Puavo: %s\n",
               e.response.to_s)
      rescue StandardError => e
        logerr(Syslog::LOG_ERR,
               "Problem sending primary user override to Puavo: %s\n",
               e.message)
      end
    end

    log(Syslog::LOG_ERR,
        "Applying primary user override: %s\n",
        primary_user_override)

    device['primary_user'] = primary_user_override
    device['conf'] ||= {}
    device['conf']['puavo.admin.primary_user'] = primary_user_override || ''
  end

  begin
    manage_local_puavoconf(device['conf'])
  rescue StandardError => e
    log(Syslog::LOG_ERR, "Error when managing local puavo-conf: %s\n",
                         e.message)
  end

  tmp_filepath = "#{ state_device_json_path }.tmp"
  File.open(tmp_filepath, 'w') { |f| f.write(device.to_json) }

  system('puavo-conf-update', '--devicejson-path', tmp_filepath)
  unless $?.exited? then
    FileUtils.rm_f(tmp_filepath)
    raise 'puavo-conf-update did not exit properly,' \
            + ' not using the new device.json'
  end

  puavo_conf_update_exit_status = $?.exitstatus
  if puavo_conf_update_exit_status == 2 then
    FileUtils.rm_f(tmp_filepath)
    raise 'puavo-conf-update returned exit code 2,' \
            + ' meaning there was an error specifically with device.json'
  end

  # We can use the new device.json even if puavo-conf-update found some error,
  # because puavo-conf-update must have handled the error.

  replace_if_changed(state_device_json_path, tmp_filepath)

  if remove_primary_user_override then
    log(Syslog::LOG_NOTICE,
        "Removing primary user override (it is the same in Puavo)\n")
    FileUtils.rm_f('/state/etc/puavo/primary_user_override')
  end

  tmp_filepath = "#{ device_json_path }.tmp"
  FileUtils.cp(state_device_json_path, tmp_filepath)
  replace_if_changed(device_json_path, tmp_filepath)

  errmsg = 'error in running puavo-conf-update: exit status' \
             + " #{ puavo_conf_update_exit_status }"
  raise errmsg unless puavo_conf_update_exit_status == 0
end

def update_org_json(restclient)
  log(Syslog::LOG_INFO, ">>> Updating org json\n")
  puavo_rest_request_and_replace(restclient,
                                 'v3/current_organisation',
                                 '/state/etc/puavo/org.json')
end

def update_external_files(restclient)
  log(Syslog::LOG_INFO, ">>> Updating external files\n")

  run('puavo-sync-external-files')
  run('puavo-handle-external-files-actions')
end

def update_grub_environment(restclient)
  log(Syslog::LOG_INFO, ">>> Updating grub environment\n")

  errors = []
  begin
    run('/etc/puavo-conf/scripts/setup_grub_default')
  rescue StandardError => e
    errors << e
  end
  begin
    run('/etc/puavo-conf/scripts/setup_grub_environment')
  rescue StandardError => e
    errors << e
  end

  raise errors.first unless errors.empty?
end

def update_printer_restrictions(restclient)
  log(Syslog::LOG_INFO, ">>> Updating printer restrictions\n")
  run('/etc/puavo-conf/scripts/setup_desktop_printer_restrictions',
      '--use-lpadmin')
end

def update_puavomenu(restclient)
  log(Syslog::LOG_INFO, ">>> Updating puavomenu\n")
  run('/etc/puavo-conf/scripts/setup_puavomenu', '--reload')
end

def update_wlan_configurations(restclient)
  log(Syslog::LOG_INFO, ">>> Updating wlan configurations\n")

  wlan_networks_uri = "v3/devices/#{ get_hostname() }/wlan_networks_with_certs"
  puavo_rest_request_and_replace(restclient,
                                 wlan_networks_uri,
                                 '/state/etc/puavo/wlan.json')
end

def update_puavopkg_installers(restclient)
  unless puavoconf_get('puavo.admin.personally_administered') == 'true' then
    log(Syslog::LOG_DEBUG,
        "Not personally administered, so not updating puavo-pkg installers\n")
    return
  end

  log(Syslog::LOG_INFO, ">>> Updating puavo-pkg installers\n")
  run('puavo-pkg-update', '--update-installers')
end


def update_certificate(restclient)
  log(Syslog::LOG_INFO, ">>> Updating host certificate\n")
  run('puavo-cert-tool', 'update')
end

def update_extra_boot_scripts(restclient)
  log(Syslog::LOG_INFO, ">>> Updating extra boot scripts\n")

  # These are fetched even if puavo.admin.run_extra_boot_scripts != 'true'
  # Error code 2 means that host was not behind a bootserver, which is okay.
  system('/usr/local/sbin/puavo-run-extra-boot-scripts', '--fetch-only')
  if $?.exitstatus != 0 && $?.exitstatus != 2 then
    raise 'Running /usr/local/sbin/puavo-run-extra-boot-scripts --fetch-only' \
            + ' returned unexpected failure'
  end
end

def update_puavoadmins(restclient)
  log(Syslog::LOG_INFO, ">>> Updating Puavo administrators\n")

  system('/usr/local/sbin/puavo-update-admins')
  if $?.exitstatus != 0 && $?.exitstatus != 2 then
    raise 'Running /usr/local/sbin/puavo-update-admins' \
            + ' returned unexpected failure'
  end
end

Syslog.open(File.basename($0), Syslog::LOG_CONS)

$puavo_hosttype = puavoconf_get('puavo.hosttype')

restclient = PuavoRestClient.new(:auth => :etc, :timeout => 30)

#
# Update several (different) things.  Even if one thing cannot be updated,
# try to update others (order does not matter here, but do this sequentially
# anyway).  Return errors on exitcodes (from exitcode one should be able
# to figure out which parts failed).
#

is_a_device = ($puavo_hosttype != 'bootserver')

# For exitcode calculation to work properly, there should not be more than
# sizeof(int) (32) functions here.  "device.json" should be updated first,
# because others may depend on it.
update_functions = [
                :update_device_json,
                :update_org_json,
  is_a_device ? :update_external_files       : :do_nothing,
  is_a_device ? :update_wlan_configurations  : :do_nothing,
                :update_grub_environment,
                :update_certificate,
                :update_extra_boot_scripts,
                :update_puavoadmins,
                :update_puavopkg_installers,
  is_a_device ? :update_printer_restrictions : :do_nothing,
  is_a_device ? :update_puavomenu            : :do_nothing,
]

exitcode = 0

unsupported_update_functions = ARGV.select do |uf|
                                 !update_functions.include?(uf.to_sym)
                               end
unless unsupported_update_functions.empty? then
  errmsg = '>>> Trying to trigger unsupported update functions: ' \
              + unsupported_update_functions.join(' ') + "\n"
  logerr(Syslog::LOG_ERR, errmsg)
end

i = 0
update_functions.each do |func|
  if ARGV.empty? || ARGV.include?(func.to_s) then
    begin
      Object.send(func, restclient)
    rescue StandardError => e
      logerr(Syslog::LOG_ERR, "Problem in running %s: %s\n", func, e.message)
      exitcode |= (1 << i)
    end
  end
  i += 1
end

Syslog.close()

exit(exitcode)
