// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
// vim: ts=8 sw=2 smarttab
/*
 * Ceph - scalable distributed file system
 *
 * Copyright (C) 2016 SUSE LINUX GmbH
 *
 * This is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License version 2.1, as published by the Free Software
 * Foundation.  See file COPYING.
 *
 */
#include "tools/rbd/ArgumentTypes.h"
#include "tools/rbd/MirrorDaemonServiceInfo.h"
#include "tools/rbd/Shell.h"
#include "tools/rbd/Utils.h"
#include "include/stringify.h"
#include "common/config.h"
#include "common/errno.h"
#include "common/Formatter.h"
#include "common/TextTable.h"
#include "global/global_context.h"
#include <iostream>
#include <boost/program_options.hpp>

namespace rbd {
namespace action {
namespace mirror_image {

namespace at = argument_types;
namespace po = boost::program_options;

namespace {

int validate_mirroring_enabled(librbd::Image &image, bool snapshot = false) {
  librbd::mirror_image_info_t mirror_image;
  int r = image.mirror_image_get_info(&mirror_image, sizeof(mirror_image));
  if (r < 0) {
    std::cerr << "rbd: failed to retrieve mirror info: "
              << cpp_strerror(r) << std::endl;
    return r;
  }

  if (mirror_image.state != RBD_MIRROR_IMAGE_ENABLED) {
    std::cerr << "rbd: mirroring not enabled on the image" << std::endl;
    return -EINVAL;
  }

  if (snapshot) {
    librbd::mirror_image_mode_t mode;
    r = image.mirror_image_get_mode(&mode);
    if (r < 0) {
      std::cerr << "rbd: failed to retrieve mirror mode: "
                << cpp_strerror(r) << std::endl;
      return r;
    }

    if (mode != RBD_MIRROR_IMAGE_MODE_SNAPSHOT) {
      std::cerr << "rbd: snapshot based mirroring not enabled on the image"
                << std::endl;
      return -EINVAL;
    }
  }

  return 0;
}

} // anonymous namespace

void get_arguments(po::options_description *positional,
                   po::options_description *options) {
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
}

void get_arguments_enable(po::options_description *positional,
                          po::options_description *options) {
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
  positional->add_options()
    ("mode", po::value<std::string>()->default_value(""),
     "mirror image mode (journal or snapshot) [default: journal]");
}

void get_arguments_disable(po::options_description *positional,
                           po::options_description *options) {
  options->add_options()
    ("force", po::bool_switch(), "disable even if not primary");
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
}

int execute_enable_disable(const po::variables_map &vm, bool enable,
                           bool force) {
  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  std::string snap_name;
  int r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, &snap_name, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  if (enable) {
    librbd::mirror_image_mode_t mode = RBD_MIRROR_IMAGE_MODE_JOURNAL;
    std::string mode_arg = utils::get_positional_argument(vm, arg_index++);
    if (mode_arg == "journal") {
      mode = RBD_MIRROR_IMAGE_MODE_JOURNAL;
    } else if (mode_arg == "snapshot") {
      mode = RBD_MIRROR_IMAGE_MODE_SNAPSHOT;
    } else if (!mode_arg.empty()) {
      std::cerr << "rbd: invalid mode name: " << mode_arg << std::endl;
      return -EINVAL;
    }
    r = image.mirror_image_enable2(mode);
  } else {
    r = image.mirror_image_disable(force);
  }
  if (r < 0) {
    return r;
  }

  std::cout << (enable ? "Mirroring enabled" : "Mirroring disabled")
    << std::endl;

  return 0;
}

int execute_disable(const po::variables_map &vm,
                    const std::vector<std::string> &ceph_global_init_args) {
  return execute_enable_disable(vm, false, vm["force"].as<bool>());
}

int execute_enable(const po::variables_map &vm,
                   const std::vector<std::string> &ceph_global_init_args) {
  return execute_enable_disable(vm, true, false);
}

void get_arguments_promote(po::options_description *positional,
                           po::options_description *options) {
  options->add_options()
    ("force", po::bool_switch(), "promote even if not cleanly demoted by remote cluster");
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
}

int execute_promote(const po::variables_map &vm,
                    const std::vector<std::string> &ceph_global_init_args) {
  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  std::string snap_name;
  int r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, &snap_name, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  bool force = vm["force"].as<bool>();

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  r = validate_mirroring_enabled(image);
  if (r < 0) {
    return r;
  }

  r = image.mirror_image_promote(force);
  if (r < 0) {
    std::cerr << "rbd: error promoting image to primary" << std::endl;
    return r;
  }

  std::cout << "Image promoted to primary" << std::endl;
  return 0;
}

int execute_demote(const po::variables_map &vm,
                   const std::vector<std::string> &ceph_global_init_args) {
  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  std::string snap_name;
  int r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, &snap_name, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  r = validate_mirroring_enabled(image);
  if (r < 0) {
    return r;
  }

  r = image.mirror_image_demote();
  if (r < 0) {
    std::cerr << "rbd: error demoting image to non-primary" << std::endl;
    return r;
  }

  std::cout << "Image demoted to non-primary" << std::endl;
  return 0;
}

int execute_resync(const po::variables_map &vm,
                   const std::vector<std::string> &ceph_global_init_args) {
  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  std::string snap_name;
  int r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, &snap_name, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  r = validate_mirroring_enabled(image);
  if (r < 0) {
    return r;
  }

  r = image.mirror_image_resync();
  if (r < 0) {
    std::cerr << "rbd: error flagging image resync" << std::endl;
    return r;
  }

  std::cout << "Flagged image for resync from primary" << std::endl;
  return 0;
}

void get_status_arguments(po::options_description *positional,
			  po::options_description *options) {
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
  at::add_format_options(options);
}

int execute_status(const po::variables_map &vm,
                   const std::vector<std::string> &ceph_global_init_args) {
  at::Format::Formatter formatter;
  int r = utils::get_formatter(vm, &formatter);
  if (r < 0) {
    return r;
  }

  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  std::string snap_name;
  r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, &snap_name, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  r = validate_mirroring_enabled(image);
  if (r < 0) {
    return r;
  }

  librados::IoCtx default_ns_io_ctx;
  default_ns_io_ctx.dup(io_ctx);
  default_ns_io_ctx.set_namespace("");

  std::vector<librbd::mirror_peer_site_t> mirror_peers;
  utils::get_mirror_peer_sites(default_ns_io_ctx, &mirror_peers);

  std::map<std::string, std::string> peer_mirror_uuids_to_name;
  utils::get_mirror_peer_mirror_uuids_to_names(mirror_peers,
                                               &peer_mirror_uuids_to_name);

  librbd::mirror_image_global_status_t status;
  r = image.mirror_image_get_global_status(&status, sizeof(status));
  if (r < 0) {
    std::cerr << "rbd: failed to get status for image " << image_name << ": "
	      << cpp_strerror(r) << std::endl;
    return r;
  }

  utils::populate_unknown_mirror_image_site_statuses(mirror_peers, &status);

  std::string instance_id;
  MirrorDaemonServiceInfo daemon_service_info(io_ctx);

  librbd::mirror_image_site_status_t local_status;
  int local_site_r = utils::get_local_mirror_image_status(
    status, &local_status);
  status.site_statuses.erase(
    std::remove_if(status.site_statuses.begin(),
                   status.site_statuses.end(),
                   [](auto& status) {
        return (status.mirror_uuid ==
                  RBD_MIRROR_IMAGE_STATUS_LOCAL_MIRROR_UUID);
      }),
    status.site_statuses.end());

  if (local_site_r >= 0 && local_status.up) {
    r = image.mirror_image_get_instance_id(&instance_id);
    if (r == -EOPNOTSUPP) {
      std::cerr << "rbd: newer release of Ceph OSDs required to map image "
                << "to rbd-mirror daemon instance" << std::endl;
      // not fatal
    } else if (r < 0 && r != -ENOENT) {
      std::cerr << "rbd: failed to get service id for image "
                << image_name << ": " << cpp_strerror(r) << std::endl;
      // not fatal
    } else if (!instance_id.empty()) {
      daemon_service_info.init();
    }
  }

  std::vector<librbd::snap_info_t> snaps;
  if (status.info.primary && status.info.state == RBD_MIRROR_IMAGE_ENABLED) {
    librbd::mirror_image_mode_t mode = RBD_MIRROR_IMAGE_MODE_JOURNAL;
    r = image.mirror_image_get_mode(&mode);
    if (r < 0) {
      std::cerr << "rbd: failed to retrieve mirror mode: "
                << cpp_strerror(r) << std::endl;
      // not fatal
    }

    if (mode == RBD_MIRROR_IMAGE_MODE_SNAPSHOT) {
      image.snap_list(snaps);
      snaps.erase(
        remove_if(snaps.begin(),
                  snaps.end(),
                  [&image](const librbd::snap_info_t &snap) {
                    librbd::snap_namespace_type_t type;
                    int r = image.snap_get_namespace_type(snap.id, &type);
                    if (r < 0) {
                      return false;
                    }
                    return type != RBD_SNAP_NAMESPACE_TYPE_MIRROR;
                  }),
        snaps.end());
    }
  }

  auto mirror_service = daemon_service_info.get_by_instance_id(instance_id);

  if (formatter != nullptr) {
    formatter->open_object_section("image");
    formatter->dump_string("name", image_name);
    formatter->dump_string("global_id", status.info.global_id);
    if (local_site_r >= 0) {
      formatter->dump_string("state", utils::mirror_image_site_status_state(
        local_status));
      formatter->dump_string("description", local_status.description);
      if (mirror_service != nullptr) {
        mirror_service->dump_image(formatter);
      }
      formatter->dump_string("last_update", utils::timestr(
        local_status.last_update));
    }
    if (!status.site_statuses.empty()) {
      formatter->open_array_section("peer_sites");
      for (auto& status : status.site_statuses) {
        formatter->open_object_section("peer_site");

        auto name_it = peer_mirror_uuids_to_name.find(status.mirror_uuid);
        formatter->dump_string("site_name",
          (name_it != peer_mirror_uuids_to_name.end() ? name_it->second : ""));
        formatter->dump_string("mirror_uuids", status.mirror_uuid);

        formatter->dump_string("state", utils::mirror_image_site_status_state(
          status));
        formatter->dump_string("description", status.description);
        formatter->dump_string("last_update", utils::timestr(
          status.last_update));
        formatter->close_section(); // peer_site
      }
      formatter->close_section(); // peer_sites
    }
    if (!snaps.empty()) {
      formatter->open_array_section("snapshots");
      for (auto &snap : snaps) {
        librbd::snap_mirror_namespace_t info;
        r = image.snap_get_mirror_namespace(snap.id, &info, sizeof(info));
        if (r < 0 ||
            (info.state != RBD_SNAP_MIRROR_STATE_PRIMARY &&
             info.state != RBD_SNAP_MIRROR_STATE_PRIMARY_DEMOTED)) {
          continue;
        }
        formatter->open_object_section("snapshot");
        formatter->dump_unsigned("id", snap.id);
        formatter->dump_string("name", snap.name);
        formatter->dump_bool("demoted",
                             info.state == RBD_SNAP_MIRROR_STATE_PRIMARY_DEMOTED);
        formatter->open_array_section("mirror_peer_uuids");
        for (auto &peer : info.mirror_peer_uuids) {
          formatter->dump_string("peer_uuid", peer);
        }
        formatter->close_section(); // mirror_peer_uuids
        formatter->close_section(); // snapshot
      }
      formatter->close_section(); // snapshots
    }
    formatter->close_section(); // image
    formatter->flush(std::cout);
  } else {
    std::cout << image_name << ":\n"
	      << "  global_id:   " << status.info.global_id << "\n";
    if (local_site_r >= 0) {
      std::cout << "  state:       " << utils::mirror_image_site_status_state(
                  local_status) << "\n"
                << "  description: " << local_status.description << "\n";
      if (mirror_service != nullptr) {
        std::cout << "  service:     " <<
          mirror_service->get_image_description() << "\n";
      }
      std::cout << "  last_update: " << utils::timestr(
        local_status.last_update) << std::endl;
    }
    if (!status.site_statuses.empty()) {
      std::cout << "  peer_sites:" << std::endl;

      bool first_site = true;
      for (auto& site : status.site_statuses) {
        if (!first_site) {
          std::cout << std::endl;
        }
        first_site = false;

        auto name_it = peer_mirror_uuids_to_name.find(site.mirror_uuid);
        std::cout << "    name: "
                  << (name_it != peer_mirror_uuids_to_name.end() ?
                        name_it->second : site.mirror_uuid)
                  << std::endl
                  << "    state: " << utils::mirror_image_site_status_state(
                    site) << std::endl
                  << "    description: " << site.description << std::endl
                  << "    last_update: " << utils::timestr(
                    site.last_update) << std::endl;
      }
    }
    if (!snaps.empty()) {
      std::cout << "  snapshots:" << std::endl;

      bool first_site = true;
      for (auto &snap : snaps) {
        librbd::snap_mirror_namespace_t info;
        r = image.snap_get_mirror_namespace(snap.id, &info, sizeof(info));
        if (r < 0 ||
            (info.state != RBD_SNAP_MIRROR_STATE_PRIMARY &&
             info.state != RBD_SNAP_MIRROR_STATE_PRIMARY_DEMOTED)) {
          continue;
        }

        if (!first_site) {
          std::cout << std::endl;
        }

        first_site = false;
        std::cout << "    " << snap.id << " " << snap.name << " ("
                  << (info.state == RBD_SNAP_MIRROR_STATE_PRIMARY_DEMOTED ?
                        "demoted " : "")
                  << "peer_uuids:[" << info.mirror_peer_uuids << "])";
      }
      std::cout << std::endl;
    }
  }

  return 0;
}

void get_snapshot_arguments(po::options_description *positional,
                            po::options_description *options) {
  at::add_image_spec_options(positional, options, at::ARGUMENT_MODIFIER_NONE);
  at::add_snap_create_options(options);
}

int execute_snapshot(const po::variables_map &vm,
                     const std::vector<std::string> &ceph_global_init_args) {
  size_t arg_index = 0;
  std::string pool_name;
  std::string namespace_name;
  std::string image_name;
  int r = utils::get_pool_image_snapshot_names(
      vm, at::ARGUMENT_MODIFIER_NONE, &arg_index, &pool_name, &namespace_name,
      &image_name, nullptr, true, utils::SNAPSHOT_PRESENCE_NONE,
      utils::SPEC_VALIDATION_NONE);
  if (r < 0) {
    return r;
  }

  uint32_t flags;
  r = utils::get_snap_create_flags(vm, &flags);
  if (r < 0) {
    return r;
  }

  librados::Rados rados;
  librados::IoCtx io_ctx;
  librbd::Image image;
  r = utils::init_and_open_image(pool_name, namespace_name, image_name, "", "",
                                 false, &rados, &io_ctx, &image);
  if (r < 0) {
    return r;
  }

  r = validate_mirroring_enabled(image, true);
  if (r < 0) {
    return r;
  }

  uint64_t snap_id;
  r = image.mirror_image_create_snapshot2(flags, &snap_id);
  if (r < 0) {
    std::cerr << "rbd: error creating snapshot: " << cpp_strerror(r)
              << std::endl;
    return r;
  }

  std::cout << "Snapshot ID: " << snap_id << std::endl;
  return 0;
}

Shell::Action action_enable(
  {"mirror", "image", "enable"}, {},
  "Enable RBD mirroring for an image.", "",
  &get_arguments_enable, &execute_enable);
Shell::Action action_disable(
  {"mirror", "image", "disable"}, {},
  "Disable RBD mirroring for an image.", "",
  &get_arguments_disable, &execute_disable);
Shell::Action action_promote(
  {"mirror", "image", "promote"}, {},
  "Promote an image to primary for RBD mirroring.", "",
  &get_arguments_promote, &execute_promote);
Shell::Action action_demote(
  {"mirror", "image", "demote"}, {},
  "Demote an image to non-primary for RBD mirroring.", "",
  &get_arguments, &execute_demote);
Shell::Action action_resync(
  {"mirror", "image", "resync"}, {},
  "Force resync to primary image for RBD mirroring.", "",
  &get_arguments, &execute_resync);
Shell::Action action_status(
  {"mirror", "image", "status"}, {},
  "Show RBD mirroring status for an image.", "",
  &get_status_arguments, &execute_status);
Shell::Action action_snapshot(
  {"mirror", "image", "snapshot"}, {},
  "Create RBD mirroring image snapshot.", "",
  &get_snapshot_arguments, &execute_snapshot);

} // namespace mirror_image
} // namespace action
} // namespace rbd
