import ./make-test-python.nix (
  { pkgs, lib, ... }:

  let
    # the single node ipv6 address
    ip = "2001:db8:ffff::";
    # the global ceph cluster id
    cluster = "54465b37-b9d8-4539-a1f9-dd33c75ee45a";
    # the fsids of OSDs
    osd-fsid-map = {
      "0" = "1c1b7ea9-06bf-4d30-9a01-37ac3a0254aa";
      "1" = "bd5a6f49-69d5-428c-ac25-a99f0c44375c";
      "2" = "c90de6c7-86c6-41da-9694-e794096dfc5c";
    };

  in
  {
    name = "basic-single-node-ceph-cluster-bluestore-dmcrypt";
    meta = {
      maintainers = with lib.maintainers; [
        benaryorg
        nh2
      ];
    };

    nodes = {
      ceph =
        { pkgs, config, ... }:
        {
          # disks for bluestore
          virtualisation.emptyDiskImages = [
            20480
            20480
            20480
          ];

          # networking setup (no external connectivity required, only local IPv6)
          networking.useDHCP = false;
          systemd.network = {
            enable = true;
            wait-online.extraArgs = [
              "-i"
              "lo"
            ];
            networks = {
              "40-loopback" = {
                enable = true;
                name = "lo";
                DHCP = "no";
                addresses = [ { Address = "${ip}/128"; } ];
              };
            };
          };

          # do not start the ceph target by default so we can format the disks first
          systemd.targets.ceph.wantedBy = lib.mkForce [ ];

          # add the packages to systemPackages so the testscript doesn't run into any unexpected issues
          # this shouldn't be required on production systems which have their required packages in the unit paths only
          # but it helps in case one needs to actually run the tooling anyway
          environment.systemPackages = with pkgs; [
            ceph
            cryptsetup
            lvm2
          ];

          services.ceph = {
            enable = true;
            client.enable = true;
            extraConfig = {
              public_addr = ip;
              cluster_addr = ip;
              # ipv6
              ms_bind_ipv4 = "false";
              ms_bind_ipv6 = "true";
              # msgr2 settings
              ms_cluster_mode = "secure";
              ms_service_mode = "secure";
              ms_client_mode = "secure";
              ms_mon_cluster_mode = "secure";
              ms_mon_service_mode = "secure";
              ms_mon_client_mode = "secure";
              # less default modules, cuts down on memory and startup time in the tests
              mgr_initial_modules = "";
              # distribute by OSD, not by host, as per https://docs.ceph.com/en/reef/cephadm/install/#single-host
              osd_crush_chooseleaf_type = "0";
            };
            client.extraConfig."mon.0" = {
              host = "ceph";
              mon_addr = "v2:[${ip}]:3300";
              public_addr = "v2:[${ip}]:3300";
            };
            global = {
              fsid = cluster;
              clusterNetwork = "${ip}/64";
              publicNetwork = "${ip}/64";
              monInitialMembers = "0";
            };

            mon = {
              enable = true;
              daemons = [ "0" ];
            };

            osd = {
              enable = true;
              daemons = builtins.attrNames osd-fsid-map;
            };

            mgr = {
              enable = true;
              daemons = [ "ceph" ];
            };
          };

          systemd.services =
            let
              osd-name = id: "ceph-osd-${id}";
              osd-pre-start = id: [
                "!${config.services.ceph.osd.package.out}/bin/ceph-volume lvm activate --bluestore ${id} ${osd-fsid-map.${id}} --no-systemd"
                "${config.services.ceph.osd.package.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${id} --cluster ${config.services.ceph.global.clusterName}"
              ];
              osd-post-stop = id: [
                "!${config.services.ceph.osd.package.out}/bin/ceph-volume lvm deactivate ${id}"
              ];
              map-osd = id: {
                name = osd-name id;
                value = {
                  serviceConfig.ExecStartPre = lib.mkForce (osd-pre-start id);
                  serviceConfig.ExecStopPost = osd-post-stop id;
                  unitConfig.ConditionPathExists = lib.mkForce [ ];
                  unitConfig.StartLimitBurst = lib.mkForce 4;
                  path = with pkgs; [
                    util-linux
                    lvm2
                    cryptsetup
                  ];
                };
              };
            in
            lib.pipe config.services.ceph.osd.daemons [
              (builtins.map map-osd)
              builtins.listToAttrs
            ];
        };
    };

    testScript =
      { ... }:
      ''
        start_all()

        ceph.wait_for_unit("default.target")

        # Bootstrap ceph-mon daemon
        ceph.succeed(
            "mkdir -p /var/lib/ceph/bootstrap-osd",
            "ceph-authtool --create-keyring /tmp/ceph.mon.keyring --gen-key -n mon. --cap mon 'allow *'",
            "ceph-authtool --create-keyring /etc/ceph/ceph.client.admin.keyring --gen-key -n client.admin --cap mon 'allow *' --cap osd 'allow *' --cap mds 'allow *' --cap mgr 'allow *'",
            "ceph-authtool --create-keyring /var/lib/ceph/bootstrap-osd/ceph.keyring --gen-key -n client.bootstrap-osd --cap mon 'profile bootstrap-osd' --cap mgr 'allow r'",
            "ceph-authtool /tmp/ceph.mon.keyring --import-keyring /etc/ceph/ceph.client.admin.keyring",
            "ceph-authtool /tmp/ceph.mon.keyring --import-keyring /var/lib/ceph/bootstrap-osd/ceph.keyring",
            "monmaptool --create --fsid ${cluster} --addv 0 'v2:[${ip}]:3300/0' --clobber /tmp/ceph.initial-monmap",
            "mkdir -p /var/lib/ceph/mon/ceph-0",
            "ceph-mon --mkfs -i 0 --monmap /tmp/ceph.initial-monmap --keyring /tmp/ceph.mon.keyring",
            "chown ceph:ceph -R /tmp/ceph.mon.keyring /var/lib/ceph",
            "systemctl start ceph-mon-0.service",
        )

        ceph.wait_for_unit("ceph-mon-0.service")
        # should the mon not start or bind for some reason this gives us a better error message than the config commands running into a timeout
        ceph.wait_for_open_port(3300, "${ip}")
        ceph.succeed(
            # required for HEALTH_OK
            "ceph config set mon auth_allow_insecure_global_id_reclaim false",
            # IPv6
            "ceph config set global ms_bind_ipv4 false",
            "ceph config set global ms_bind_ipv6 true",
            # the new (secure) protocol
            "ceph config set global ms_bind_msgr1 false",
            "ceph config set global ms_bind_msgr2 true",
            # just a small little thing
            "ceph config set mon mon_compact_on_start true",
        )

        # Can't check ceph status until a mon is up
        ceph.succeed("ceph -s | grep 'mon: 1 daemons'")

        # Bootstrap OSDs (do this before starting the mgr because cryptsetup and the mgr both eat a lot of memory)
        ceph.succeed(
            # this will automatically do what's required for LVM, cryptsetup, and stores all the data in Ceph's internal databases
            "ceph-volume lvm prepare --bluestore --data /dev/vdb --dmcrypt --no-systemd --osd-id 0 --osd-fsid ${osd-fsid-map."0"}",
            "ceph-volume lvm prepare --bluestore --data /dev/vdc --dmcrypt --no-systemd --osd-id 1 --osd-fsid ${osd-fsid-map."1"}",
            "ceph-volume lvm prepare --bluestore --data /dev/vdd --dmcrypt --no-systemd --osd-id 2 --osd-fsid ${osd-fsid-map."2"}",
            "sudo ceph-volume lvm deactivate 0",
            "sudo ceph-volume lvm deactivate 1",
            "sudo ceph-volume lvm deactivate 2",
            "chown -R ceph:ceph /var/lib/ceph",
        )

        # Start OSDs (again, argon2id eats memory, so this happens before starting the mgr)
        ceph.succeed(
            "systemctl start ceph-osd-0.service",
            "systemctl start ceph-osd-1.service",
            "systemctl start ceph-osd-2.service",
        )
        ceph.wait_until_succeeds("ceph -s | grep 'quorum 0'")
        ceph.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")

        # Start the ceph-mgr daemon, after copying in the keyring
        ceph.succeed(
            "mkdir -p /var/lib/ceph/mgr/ceph-ceph/",
            "ceph auth get-or-create -o /var/lib/ceph/mgr/ceph-ceph/keyring mgr.ceph mon 'allow profile mgr' osd 'allow *' mds 'allow *'",
            "chown -R ceph:ceph /var/lib/ceph/mgr/ceph-ceph/",
            "systemctl start ceph-mgr-ceph.service",
        )
        ceph.wait_for_unit("ceph-mgr-ceph")
        ceph.wait_until_succeeds("ceph -s | grep 'quorum 0'")
        ceph.wait_until_succeeds("ceph -s | grep 'mgr: ceph(active,'")
        ceph.wait_until_succeeds("ceph osd stat | grep -e '3 osds: 3 up[^,]*, 3 in'")
        ceph.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")

        # test the actual storage
        ceph.succeed(
            "ceph osd pool create single-node-test 32 32",
            "ceph osd pool ls | grep 'single-node-test'",

            # We need to enable an application on the pool, otherwise it will
            # stay unhealthy in state POOL_APP_NOT_ENABLED.
            # Creating a CephFS would do this automatically, but we haven't done that here.
            # See: https://docs.ceph.com/en/reef/rados/operations/pools/#associating-a-pool-with-an-application
            # We use the custom application name "nixos-test" for this.
            "ceph osd pool application enable single-node-test nixos-test",

            "ceph osd pool rename single-node-test single-node-other-test",
            "ceph osd pool ls | grep 'single-node-other-test'",
        )
        ceph.wait_until_succeeds("ceph -s | grep '2 pools, 33 pgs'")
        ceph.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
        ceph.wait_until_succeeds("ceph -s | grep '33 active+clean'")
        ceph.fail(
            # the old pool should be gone
            "ceph osd pool ls | grep 'multi-node-test'",
            # deleting the pool should fail without setting mon_allow_pool_delete
            "ceph osd pool delete single-node-other-test single-node-other-test --yes-i-really-really-mean-it",
        )

        # rebooting gets rid of any potential tmpfs mounts or device-mapper devices
        ceph.shutdown()
        ceph.start()
        ceph.wait_for_unit("default.target")

        # Start it up (again OSDs first due to memory constraints of cryptsetup and mgr)
        ceph.systemctl("start ceph-mon-0.service")
        ceph.wait_for_unit("ceph-mon-0")
        ceph.systemctl("start ceph-osd-0.service")
        ceph.wait_for_unit("ceph-osd-0")
        ceph.systemctl("start ceph-osd-1.service")
        ceph.wait_for_unit("ceph-osd-1")
        ceph.systemctl("start ceph-osd-2.service")
        ceph.wait_for_unit("ceph-osd-2")
        ceph.systemctl("start ceph-mgr-ceph.service")
        ceph.wait_for_unit("ceph-mgr-ceph")

        # Ensure the cluster comes back up again
        ceph.succeed("ceph -s | grep 'mon: 1 daemons'")
        ceph.wait_until_succeeds("ceph -s | grep 'quorum 0'")
        ceph.wait_until_succeeds("ceph osd stat | grep -E '3 osds: 3 up[^,]*, 3 in'")
        ceph.wait_until_succeeds("ceph -s | grep 'mgr: ceph(active,'")
        ceph.wait_until_succeeds("ceph -s | grep 'HEALTH_OK'")
      '';
  }
)
