(* virt-v2v
 * Copyright (C) 2009-2020 Red Hat Inc.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *)

open Unix
open Printf

open Common_gettext.Gettext
open Std_utils
open Tools_utils
open Unix_utils

open Utils

let is_installed =
  let test = lazy (Sys.command "nbdkit --version >/dev/null 2>&1" = 0) in
  fun () -> Lazy.force test

type config = (string * string) list

let get_config = lazy (
  let cmd = "nbdkit --dump-config" in
  let lines = external_command cmd in
  List.map (String.split "=") lines
)
let config () = Lazy.force get_config

type version = int * int * int

let rex = PCRE.compile "^(\\d+)\\.(\\d+)\\.(\\d+)"
let get_version = lazy (
  let config = config () in
  let version =
    try List.assoc "version" config
    with Not_found -> failwith "nbdkit: no version in --dump-config" in
  if not (PCRE.matches rex version) then
    error (f_"nbdkit: unexpected version in --dump-config: %s") version;
  let major = int_of_string (PCRE.sub 1)
  and minor = int_of_string (PCRE.sub 2)
  and release = int_of_string (PCRE.sub 3) in
  if verbose () then (
    eprintf "info: nbdkit version:\n%!";
    ignore (Sys.command "nbdkit --version >&2");
    eprintf "parsed version: %d.%d.%d\n%!" major minor release
  );
  (major, minor, release)
)
let version () = Lazy.force get_version

let probe_plugin name =
  let cmd = sprintf "nbdkit %s --version >/dev/null 2>&1" (quote name) in
  Sys.command cmd = 0

let probe_filter name =
  let cmd = sprintf "nbdkit null --filter=%s --version >/dev/null 2>&1"
              (quote name) in
  Sys.command cmd = 0

let probe_filter_parameter name regex =
  let cmd = sprintf "nbdkit null --filter=%s --help | grep -sq %s"
              (quote name) (quote regex) in
  Sys.command cmd = 0

type cmd = {
  plugin : string;
  mutable filters : string list;
  mutable args : (string * string) list; (* stored reversed *)
  mutable env : (string * string) list;
  mutable debug_flags : (string * string) list;
  mutable readonly : bool;
  mutable threads : int;
  verbose : bool;
}

let create ?(quiet = false) plugin = {
  plugin;
  filters = [];
  args = [];
  env = [ "LANG", "C" ];
  debug_flags = [];
  readonly = false;
  threads = 16;
  verbose = not quiet && verbose ()
}

let add_debug_flag cmd name value =
  cmd.debug_flags <- (name, value) :: cmd.debug_flags

let set_readonly cmd v = cmd.readonly <- v
let set_threads cmd v = cmd.threads <- v
let add_filter cmd v = cmd.filters <- v :: cmd.filters
let add_arg cmd key value = cmd.args <- (key, value) :: cmd.args
let add_args cmd kvs = cmd.args <- List.rev kvs @ cmd.args
let add_env cmd name value = cmd.env <- (name, value) :: cmd.env

let add_filter_if_available cmd filter =
  if probe_filter filter then add_filter cmd filter

let run_unix socket cmd =
  (* Create a temporary directory where we place the PID file. *)
  let piddir = Mkdtemp.temp_dir "v2vnbdkit." in
  On_exit.rm_rf piddir;

  let id = unique () in
  let pidfile = piddir // sprintf "nbdkit%d.pid" id in

  (* Construct the final command line. *)
  let add_arg, add_args_reversed, get_args =
    let r = ref [] in
    let add_arg v = List.push_front v r in
    let add_args_reversed vs = r := vs @ !r in
    let get_args () = List.rev !r in
    add_arg, add_args_reversed, get_args
  in

  add_arg "nbdkit";
  add_arg "--exit-with-parent";
  add_arg "--foreground";
  add_arg "--pidfile"; add_arg pidfile;
  add_arg "--unix"; add_arg socket;
  add_arg "--threads"; add_arg (string_of_int cmd.threads);

  if have_selinux then (
    add_arg "--selinux-label";
    add_arg "system_u:object_r:svirt_socket_t:s0"
  );

  (* Reduce verbosity in nbdkit >= 1.17.4. *)
  let version = version () in
  if version >= (1, 17, 4) then (
    add_arg "-D"; add_arg "nbdkit.backend.datapath=0"
  );

  List.iter (
    fun (name, value) ->
      add_arg "-D"; add_arg (sprintf "%s=%s" name value)
  ) cmd.debug_flags;
  if cmd.readonly then add_arg "--readonly";
  if cmd.verbose then add_arg "--verbose";
  List.iter (fun filter -> add_arg "--filter"; add_arg filter) cmd.filters;

  add_arg cmd.plugin;
  add_args_reversed (List.map (fun (k, v) -> sprintf "%s=%s" k v) cmd.args);
  let args = get_args () in

  (* Print the full command we are about to run when debugging. *)
  if verbose () then (
    eprintf "running nbdkit:\n";
    List.iter (fun (k, v) -> eprintf " %s=%s" k v) cmd.env;
    List.iter (fun arg -> eprintf " %s" (quote arg)) args;
    prerr_newline ()
  );

  (* Start an nbdkit instance in the background. *)
  let args = Array.of_list args in
  let pid = fork () in
  if pid = 0 then (
    (* Child process (nbdkit). *)
    List.iter (fun (k, v) -> putenv k v) cmd.env;
    execvp "nbdkit" args
  );

  (* Wait for the pidfile to appear so we know that nbdkit
   * is listening for requests.
   *)
  if not (wait_for_file pidfile 30) then (
    if verbose () then
      error (f_"nbdkit did not start up.  See previous debugging messages for problems.")
    else
      error (f_"nbdkit did not start up.  There may be errors printed by nbdkit above.

If the messages above are not sufficient to diagnose the problem then add the ‘virt-v2v -v -x’ options and examine the debugging output carefully.")
  );

  if have_selinux then (
    (* Note that Unix domain sockets have both a file label and
     * a socket/process label.  Using --selinux-label above
     * only set the socket label, but we must also set the file
     * label.
     *)
    ignore (run_command ["chcon"; "system_u:object_r:svirt_image_t:s0";
                         socket]);
  );

  (* Set the regular Unix permissions, in case nbdkit is
   * running as another user.
   *)
  chown_for_libvirt_rhbz_1045069 socket;
  chmod socket 0o700;

  socket, pid
