public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: "Michael Köppl" <m.koeppl@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [PATCH e2e-tests 29/38] tests: ceph: verify replacing an OSD
Date: Wed,  4 Nov 2026 20:03:36 +0100	[thread overview]
Message-ID: <20261104190345.2573464-30-m.koeppl@proxmox.com> (raw)
In-Reply-To: <20261104190345.2573464-1-m.koeppl@proxmox.com>

Replace an OSD as the documentation describes, while a container
runs on an RBD storage. Mark the OSD out, wait until its placement
groups have moved, check that it is safe to stop and stop it, then
destroy it with cleanup and create a new OSD on the same disk.

Destroying with cleanup must remove the OSD from the OSD tree, leave
neither an enabled service nor a data directory behind and leave the
disk unused, so that it can be reused right away. Afterwards, the
cluster must be healthy with all OSDs up and in, and the container
must have kept running with its data intact.

Signed-off-by: Michael Köppl <m.koeppl@proxmox.com>
---
 tests/ceph/test_ceph_osd_replace.pl | 237 ++++++++++++++++++++++++++++
 tests/testcases.ron                 |  11 ++
 2 files changed, 248 insertions(+)
 create mode 100755 tests/ceph/test_ceph_osd_replace.pl

diff --git a/tests/ceph/test_ceph_osd_replace.pl b/tests/ceph/test_ceph_osd_replace.pl
new file mode 100755
index 0000000..51857ec
--- /dev/null
+++ b/tests/ceph/test_ceph_osd_replace.pl
@@ -0,0 +1,237 @@
+#!/usr/bin/perl
+
+=head1 NAME
+
+C<test_ceph_osd_replace> - Test replacing an OSD of a hyper-converged Ceph cluster.
+
+=head1 DESCRIPTION
+
+This testcase sets up Ceph on the cluster nodes with a pool and an RBD storage
+and starts a container on that storage. It then replaces an OSD on the second
+node as the documentation describes. It marks the OSD out, waits until Ceph has
+moved all placement groups off it, checks that it is safe to stop and stops it.
+It destroys the OSD with cleanup, which must remove the OSD from the OSD tree,
+leave neither an enabled service nor a data directory of it behind and leave
+its disk unused, so that a new OSD can be created on the same disk right away.
+
+Afterwards, the cluster must report HEALTH_OK with all OSDs up and in again.
+The container must have kept running with its data intact.
+
+=cut
+
+use strict;
+use warnings;
+
+use Test::More;
+
+use lib '.';
+use Proxmox::Test::Ceph qw(
+    ceph_disks
+    ceph_osds
+    ceph_status
+    create_ceph_cluster
+    create_pool
+    wait_for_ceph
+);
+use Proxmox::Test::Helper qw(CONTAINER_TEMPLATE get_pve_instances);
+use Proxmox::Test::Testcase;
+use Proxmox::Test::Util qw(poll_until);
+
+my $pve_instances = get_pve_instances();
+die "not enough PVE instances" if scalar($pve_instances->@*) < 3;
+
+my $first = $pve_instances->[0];
+my $second = $pve_instances->[1];
+my $node = $first->get_nodename();
+my $osd_node = $second->get_nodename();
+my $client = $first->client();
+my $osd_client = $second->client();
+
+my $pool = 'e2e-replace';
+my $marker = 'e2e-osd-replace';
+
+my $osds;
+my $ctid;
+my $osd = {};
+
+# The entry of osd.$id in the OSD tree of the second (!) node, or undef if it is not listed.
+my $osd_entry = sub {
+    my ($id) = @_;
+    my ($entry) = grep { $_->{id} == $id } ceph_osds($second)->@*;
+    return $entry;
+};
+
+# The entry of the disk $devpath in the disk list of the second (!) node.
+my $disk_entry = sub {
+    my ($devpath) = @_;
+    my ($entry) =
+        grep { $_->{devpath} eq $devpath } $osd_client->get("/nodes/$osd_node/disks/list", {})->@*;
+    return $entry // {};
+};
+
+Proxmox::Test::Testcase->new('ceph_osd_replace')->before_all(sub {
+    $osds = create_ceph_cluster($pve_instances);
+    create_pool($first, $pool, add_storages => 1);
+})->with([{}])->step(
+        'start container on RBD storage',
+        sub {
+            $ctid = $client->get('/cluster/nextid');
+            my $upid = $client->post(
+                "/nodes/$node/lxc",
+                {
+                    vmid => $ctid,
+                    ostemplate => CONTAINER_TEMPLATE,
+                    rootfs => "$pool:2",
+                    memory => 256,
+                    unprivileged => 1,
+                    features => 'nesting=1',
+                    hostname => 'ct-osd-replace',
+                },
+            );
+            $first->task_ok($upid, "created container $ctid on '$pool'")
+            or die "creating the container failed\n";
+
+            $first->start_guest('lxc', $ctid);
+            $first->ct_exec($ctid, "sh -c 'echo $marker > /root/marker'");
+        },
+        sub {
+            $first->destroy_guest('lxc', $ctid) if defined($ctid);
+        },
+)->step(
+    'mark OSD out',
+    sub {
+        my ($entry) = ceph_osds($second)->@*;
+        die "no OSD on '$osd_node'\n" if !$entry;
+        my $id = $osd->{id} = $entry->{id};
+
+        my ($disk) = grep { ($_->{osdid} // -1) == $id }
+            $osd_client->get("/nodes/$osd_node/disks/list", {})->@*;
+        $osd->{disk} = ($disk // {})->{devpath};
+        ok($osd->{disk}, "disk list shows the disk of osd.$id") or die "disk not found\n";
+
+        $osd_client->post("/nodes/$osd_node/ceph/osd/$id/out", {});
+
+        poll_until(
+            sub { !($osd_entry->($id) // {})->{in} },
+            timeout => 30,
+            interval => 2,
+            description => "osd.$id to be out",
+        );
+        pass("osd.$id is out");
+    },
+)->step(
+    'wait for the data to move off the OSD',
+    sub {
+        my $id = $osd->{id};
+
+        poll_until(
+            sub {
+                return 0 if (($osd_entry->($id) // {})->{pgs} // -1) != 0;
+
+                my $pgmap = ceph_status($first)->{pgmap} // {};
+                my ($clean) = map { $_->{count} }
+                    grep { $_->{state_name} eq 'active+clean' } ($pgmap->{pgs_by_state} // [])->@*;
+                return ($clean // 0) == ($pgmap->{num_pgs} // -1);
+            },
+            timeout => 600,
+            interval => 5,
+            description => "osd.$id to hold no placement groups",
+        );
+        pass("Ceph moved all placement groups off osd.$id");
+    },
+)->step(
+    'stop OSD',
+    sub {
+        my $id = $osd->{id};
+
+        my $safety = $osd_client->get(
+            "/nodes/$osd_node/ceph/cmd-safety",
+            { service => 'osd', id => $id, action => 'stop' },
+        );
+        ok($safety->{safe}, "Ceph reports that stopping osd.$id is safe")
+            or diag($safety->{status} // 'no status');
+
+        my $upid = $osd_client->post("/nodes/$osd_node/ceph/stop", { service => "osd.$id" });
+        $second->task_ok($upid, "stopped osd.$id") or die "stopping osd.$id failed\n";
+
+        poll_until(
+            sub { (($osd_entry->($id) // {})->{status} // '') eq 'down' },
+            timeout => 60,
+            interval => 2,
+            description => "osd.$id to be down",
+        );
+        pass("osd.$id is down");
+    },
+)->step(
+    'destroy OSD with cleanup',
+    sub {
+        my $id = $osd->{id};
+
+        my $upid = $osd_client->delete("/nodes/$osd_node/ceph/osd/$id", { cleanup => 1 });
+        $second->task_ok($upid, "destroyed osd.$id") or die "destroying osd.$id failed\n";
+
+        ok(!$osd_entry->($id), "osd.$id is gone from the OSD tree");
+
+        my ($enabled) =
+            $second->run_command("systemctl is-enabled ceph-osd\@$id.service || true");
+        unlike($enabled // '', qr/^enabled/, "service of osd.$id is no longer enabled");
+        ok(
+            eval { $second->run_command("test ! -e /var/lib/ceph/osd/ceph-$id"); 1 },
+            "data directory of osd.$id is removed",
+        );
+
+        # The change in the disk list is not immediately reflected.
+        my $disk;
+        eval {
+            poll_until(
+                sub {
+                    $disk = $disk_entry->($osd->{disk});
+                    return !$disk->{used} && ($disk->{osdid} // -1) == -1;
+                },
+                timeout => 60,
+                interval => 2,
+                description => "disk '$osd->{disk}' to be unused",
+            );
+        };
+        ok(!$disk->{used}, "disk '$osd->{disk}' is unused")
+            or diag("disk is used by: $disk->{used}");
+        is($disk->{osdid} // -1, -1, "disk '$osd->{disk}' no longer belongs to an OSD");
+    },
+)->step(
+    'create OSD on the same disk',
+    sub {
+        my $upid = $osd_client->post("/nodes/$osd_node/ceph/osd", { dev => $osd->{disk} });
+        $second->task_ok($upid, "created OSD on '$osd->{disk}' again")
+            or die "creating the OSD failed\n";
+
+        wait_for_ceph(
+            $first,
+            monitors => scalar($pve_instances->@*),
+            osds => $osds,
+            health_ok => 1,
+        );
+        pass("the Ceph cluster reports HEALTH_OK with all $osds OSDs up and in");
+
+        is(
+            scalar(ceph_osds($second)->@*),
+            scalar(ceph_disks($second)->@*),
+            "'$osd_node' has an OSD on every Ceph disk again",
+        );
+    },
+)->step(
+    'container kept running',
+    sub {
+        is(
+            $client->get("/nodes/$node/lxc/$ctid/status/current", {})->{status},
+            'running',
+            "container $ctid kept running",
+        );
+        is($first->ct_exec($ctid, 'cat /root/marker'), "$marker\n", 'container data is intact');
+        ok(
+            eval { $first->ct_exec($ctid, "sh -c 'echo written > /root/after-replace'"); 1 },
+            'container can still write to its volume',
+        );
+    },
+)->run();
+
+done_testing();
diff --git a/tests/testcases.ron b/tests/testcases.ron
index 9a94d11..340ac81 100644
--- a/tests/testcases.ron
+++ b/tests/testcases.ron
@@ -425,6 +425,17 @@ TestcaseConfig(
             ],
             tag: "ceph",
         ),
+        Testcase(
+            name: "ceph_osd_replace",
+            run: "./tests/ceph/test_ceph_osd_replace.pl",
+            timeout: 1800,
+            instances: [
+                "pve1-cluster",
+                "pve2-cluster",
+                "pve3-cluster",
+            ],
+            tag: "ceph",
+        ),
     ],
     rollback_between_tests: Some("pre-test"),
 )
-- 
2.47.3





  parent reply	other threads:[~2026-11-04 19:07 UTC|newest]

Thread overview: 47+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-04 19:03 [PATCH e2e-tests/test-tools 00/38] tests: add tests for guests, Ceph, PBS and cluster workflows Michael Köppl
2026-11-04 19:03 ` [PATCH test-tools 01/38] ansible: rename snd_test bridge to tt_test as multi-purpose bridge Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 02/38] plans: rename sdn_test bridge to tt_test Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 03/38] debian: add missing jq dependency Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 04/38] test framework: move the default container template to the helper Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 05/38] lib: instance: add helper to check if an address answers ping Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 06/38] lib: pveinstance: look up disks by their SCSI serial Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 07/38] lib: pveinstance: add helper to find a task started by another task Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 08/38] lib: pveinstance: add helpers to start and stop guests Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 09/38] lib: pveinstance: add helper to run commands in containers Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 10/38] lib: pveinstance: add helper to wait for qmeventd's cleanup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 11/38] lib: pveinstance: add helper to check if guest exists Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 12/38] lib: add helpers to use a PBS datastore as PVE storage Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 13/38] scripts: retry container template download, fail if it fails Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 14/38] tests: container: verify lifecycle and guest setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 15/38] tests: container: verify volume and bind mountpoints Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 16/38] tests: container: verify snapshots, clones, templates Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 17/38] tests: container: verify device passthrough Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 18/38] plans,scripts: build a minimal OCI image onto pve1 Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 19/38] tests: container: verify application containers from OCI images Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 20/38] tests: vm: verify snapshots, clones, templates Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 21/38] tests: vm: verify adding, resizing, and removing disks Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 22/38] plans,scripts: download a Debian cloud image onto pve1 Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 23/38] tests: vm: verify lifecycle operations and cloud-init guest setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 24/38] plans,scripts: install Ceph Tentacle on the cluster nodes Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 25/38] test framework: add Ceph test helpers Michael Köppl
2026-11-06 12:24   ` Kefu Chai
2026-11-06 13:04     ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 26/38] tests: ceph: verify setting up a hyper-converged cluster Michael Köppl
2026-11-06 12:50   ` Kefu Chai
2026-11-06 13:05     ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 27/38] tests: ceph: verify guests on RBD storage Michael Köppl
2026-11-06 13:01   ` Kefu Chai
2026-11-06 13:07     ` Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 28/38] tests: ceph: verify setting up, using and destroying a CephFS Michael Köppl
2026-11-04 19:03 ` Michael Köppl [this message]
2026-11-06 13:14   ` [PATCH e2e-tests 29/38] tests: ceph: verify replacing an OSD Kefu Chai
2026-11-04 19:03 ` [PATCH e2e-tests 30/38] tests: ceph: drop the outdated Ceph setup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 31/38] tests: pbs: verify container backup and restore Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 32/38] tests: backup: verify VM backup and restore, locally and on PBS Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 33/38] tests: pbs: verify single file restore from a container backup Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 34/38] tests: storage: verify moving guest volumes between storages Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 35/38] tests: migration: verify container migration between nodes Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 36/38] tests: migration: verify VM migration with local disks Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 37/38] tests: ha: verify moving guests off a node for maintenance Michael Köppl
2026-11-04 19:03 ` [PATCH e2e-tests 38/38] tests: migration: verify migrating guests in bulk Michael Köppl
2026-11-06 13:25 ` [PATCH e2e-tests/test-tools 00/38] tests: add tests for guests, Ceph, PBS and cluster workflows Kefu Chai

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261104190345.2573464-30-m.koeppl@proxmox.com \
    --to=m.koeppl@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal