#!/usr/bin/ruby

require 'yaml'
require 'json'
require 'fileutils'
require 'logger'

class LMH
  def initialize(argv)
    @version = "0.0.2"

    @loglog = Logger.new(STDERR)
    @loglog.level = Logger::INFO
    @loglog.info("Welcome to #{$0} v#{@version}")
    @config_path = "/etc/lmh.yaml"
    @config = YAML.load(File.read(@config_path))
    command = argv.shift
    @argv   = argv

    case command
    when 'create'
      clone_new_machine
    when 'restart-all', 'ra'
      restart_all
    when 'stop-all', 'ka'
      stop_all
    when 'start-all', 'sa'
      start_all
    when 'tmux-shell-all', 'tsa'
      tmux_shell
    when 'help', '--help'
      usage
    when 'version', '--version'
    else
      @loglog.error("Unhandled command #{command}") unless command.nil?
      usage
    end
  end

  def usage
    usage = <<EOF
Usage:
  #{$0} [create|start-all|restart-all|stop-all|tmux-shell-all] ARGV

  Short hands:

  restart-all    - ra
  start-all      - sa
  stop-all       - ka
  tmux-shell-all - tsa

  start-all/restart-all/stop-all-tmux-shell-all accept a list of machines that should be acted on

  For start-all/restart-all you can configure base_images in /etc/lmh.yaml, which will not be started.

  ```
  base_images:
    - tumbleweed
    - leap16.0
  ```

  #{$0} create source_machine machinename [hostname]

  #{$0} create tumbleweed matrix.dev.example.com
  #{$0} create tumbleweed matrix-dev matrix.dev.example.com
EOF
    @loglog.info(usage)
    exit(0)
  end

  def clone_new_machine
    if @argv.length < 2 or @argv.length > 3
      usage
    end
    @source_machine, @machine_name, @machin_hostname = @argv
    if @machin_hostname.nil?
      @machin_hostname = @machine_name
    end
    @machine_dir = File.join("/var/lib/machines", @machine_name)
    ensure_root && \
    ensure_base_machine && \
    cloning_machine && \
    configure_resolved && \
    configure_salt && \
    configure_hostname && \
    @loglog.info("Machine #{@machine_name} is ready to be started")
    system('machinectl', 'start', @machine_name)
  end


  def get_all_non_base_images
    return @argv unless @argv.empty?
    all_machines = []
    IO.popen(['machinectl', 'list-images', '--output=json']) do |list_fd|
      all_machines = JSON.load(list_fd.read).map {|e| e['name'] }
    end
    all_machines = all_machines - @config.fetch('base_images', [])
    all_machines.sort
  end


  def get_all_running
    return @argv unless @argv.empty?
    all_machines = []
    IO.popen(['machinectl', 'list', '--output=json']) do |list_fd|
      all_machines = JSON.load(list_fd.read).map {|e| e['machine'] }
    end
    all_machines.sort
  end

  def restart_all
    all_machines = get_all_running
    if all_machines.empty?
      start_all
    else
      get_all_running.each do |machine_name|
        @loglog.info "Restarting #{machine_name}"
        system('machinectl', 'restart', machine_name)
      end
    end
  end


  def start_all
    get_all_non_base_images.each do |machine_name|
      @loglog.info "Starting #{machine_name}"
      system('machinectl', 'start', machine_name)
    end
  end

  def tmux_shell
    get_all_running.each do |machine_name|
      @loglog.info "Spawning shell for #{machine_name}"
      system('tmux', 'new-window', 'machinectl', 'shell', machine_name)
    end
  end

  def stop_all
    get_all_running.each do |machine_name|
      @loglog.info "Stopping #{machine_name}"
      system('machinectl', 'stop', machine_name)
    end
  end

  def exit_with_error(error_code, error_message)
    @loglog.error(error_message)
    exit(error_code)
  end

  def ensure_root
    if Process.euid != 0
      exit_with_error(2, "This script requires root privileges. Exiting.")
    end
    return true
  end

  def ensure_base_machine
    unless File.exist?(File.join("/var/lib/machines", @source_machine))
      exit_with_error(9, "Base machine #{@source_machine} does not exist")
    end
    return true
  end

  def cloning_machine
    if File.exist?(@machine_dir)
      @loglog.info("Machine #{@machine_dir} already exists. Not cloning again.")
      return true
    end
    if system('machinectl', 'clone', @source_machine, @machine_name)
      @loglog.info("Succesfully cloned machine to #{@machine_dir}")
      return true
    else
      exit_with_error(3, 'Cloning failed!')
    end
  end

  def configure_resolved
    if @config.include?('resolveconf')
      resolved_config_drop_in = File.join(@machine_dir, 'etc/systemd/resolved.conf.d/lmh.conf')
      config = (['[Resolve]'] + @config['resolveconf'].map {|k,v| "#{k}=#{v}"}).join("\n")
      File.write(resolved_config_drop_in, config)
      return true
    else
      @loglog.info("No resolved config found")
      return true
    end
  rescue Exception => ex
    exit_with_error(4, "Something failed while writing the resolved config droplet: #{ex}")
  end

  def configure_salt
    salt_minion_service_path = '/usr/lib/systemd/system/salt-minion.service'
    if File.exist?(File.join(@machine_dir, salt_minion_service_path))
      File.write(File.join(@machine_dir, "etc/salt/minion_id"), @machin_hostname)
      if @config.include?('saltmaster')
        @loglog.info("Pointing saltmaster to #{@config['saltmaster']}")
        File.write(File.join(@machine_dir, "etc/salt/minion.d/salt_managed.conf"), "master: #{@config['saltmaster']}")
      end
      target_symlink = File.join(@machine_dir, 'etc/systemd/system/multi-user.target.wants/salt-minion.service')
      unless File.exist?(target_symlink)
        unless File.symlink(salt_minion_service_path, target_symlink)
          exit_with_error(5, "Enabling salt in the machine failed")
        end
      else
         @loglog.info("salt minion already enabled")
      end
      if @config.fetch('salt_create_cfgmgmt_minion_file', true)
        minion_file_path = File.join('/srv/cfgmgmt/pillar/minions/', @machin_hostname.gsub(/\./, '_') + '.sls')
        if File.exist?(minion_file_path)
          @loglog.info("minion file already exists #{minion_file_path}")
        else
          @loglog.info("Setting up empty #{minion_file_path}")
          File.write(minion_file_path, "")
        end
      end
      return true
    else
      @loglog.error("Salt not installed in base image?")
      return true
    end
  rescue Exception => ex
    exit_with_error(6, "Something failed configuring salt: #{ex}")
  end

  def configure_hostname
    unless File.write(File.join(@machine_dir, 'etc/hostname'), @machin_hostname)
      exit_with_error(7, "Writing hostname failed")
    else
      @loglog.info("Set hostname to #{@machin_hostname}")
    end
    return true
  end

end

lmh = LMH.new(ARGV)