forked from rook/rook
archiveCrash is meant to silence the RECENT_CRASH health warning for a removed OSD by archiving its crash, but the early-return guard was inverted. GetCrashList unmarshals `ceph crash ls` into a non-nil slice on every success, so `if crash != nil` was always true. The function always logged "no ceph crash to silence" and the archive loop was unreachable, so OSD removal never archived the crash and the warning lingered. Guard on `len(crash) == 0` so only a genuinely empty list short-circuits. Reviving the loop surfaced two latent bugs it had always masked. On a non-empty list with no entry for the removed OSD the crash id stayed empty and the code ran `ceph crash archive ""`, which the mgr rejects with EINVAL and logs as a spurious error during routine OSD removal, so skip the archive when no crash matches. The loop also broke on the first match, but `ceph crash ls` is oldest-first and includes already-archived entries, so an OSD with several crashes had only its oldest one archived while RECENT_CRASH persisted; archive every matching entry instead of just the first. Extend TestArchiveCrash to cover all four cases: a single matching crash, a list with several matching crashes that are all archived, an empty list, and a non-empty list with no matching entry where nothing is archived. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> Signed-off-by: Joshua Hoblitt <josh@hoblitt.com>
293 lines
12 KiB
Go
293 lines
12 KiB
Go
/*
|
|
Copyright 2020 The Rook Authors. All rights reserved.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package osd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"time"
|
|
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/pkg/errors"
|
|
"github.com/rook/rook/pkg/clusterd"
|
|
"github.com/rook/rook/pkg/daemon/ceph/client"
|
|
oposd "github.com/rook/rook/pkg/operator/ceph/cluster/osd"
|
|
"github.com/rook/rook/pkg/operator/k8sutil"
|
|
)
|
|
|
|
// RemoveOSDs purges a list of OSDs from the cluster
|
|
func RemoveOSDs(context *clusterd.Context, clusterInfo *client.ClusterInfo, osdsToRemove []string, preservePVC, forceOSDRemoval bool) error {
|
|
// Generate the ceph config for running ceph commands similar to the operator
|
|
if err := client.WriteCephConfig(context, clusterInfo); err != nil {
|
|
return errors.Wrap(err, "failed to write the ceph config")
|
|
}
|
|
|
|
osdDump, err := client.GetOSDDump(context, clusterInfo)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to get osd dump")
|
|
}
|
|
|
|
for _, osdIDStr := range osdsToRemove {
|
|
osdID, err := strconv.Atoi(osdIDStr)
|
|
if err != nil {
|
|
logger.Errorf("invalid OSD ID: %s. %v", osdIDStr, err)
|
|
continue
|
|
}
|
|
logger.Infof("validating status of osd.%d", osdID)
|
|
status, _, err := osdDump.StatusByID(int64(osdID))
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get osd status for osd %d", osdID)
|
|
}
|
|
const upStatus int64 = 1
|
|
if status == upStatus {
|
|
logger.Infof("osd.%d is healthy. It cannot be removed unless it is 'down'", osdID)
|
|
continue
|
|
} else {
|
|
logger.Infof("osd.%d is marked 'DOWN'", osdID)
|
|
}
|
|
|
|
removeOSD(context, clusterInfo, osdID, preservePVC, forceOSDRemoval)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func removeOSD(clusterdContext *clusterd.Context, clusterInfo *client.ClusterInfo, osdID int, preservePVC, forceOSDRemoval bool) {
|
|
// Get the host where the OSD is found
|
|
hostName, err := client.GetCrushHostName(clusterdContext, clusterInfo, osdID)
|
|
if err != nil {
|
|
logger.Errorf("failed to get the host where osd.%d is running. %v", osdID, err)
|
|
}
|
|
|
|
// Mark the OSD as out.
|
|
logger.Infof("marking osd.%d out", osdID)
|
|
args := []string{"osd", "out", fmt.Sprintf("osd.%d", osdID)}
|
|
_, err = client.NewCephCommand(clusterdContext, clusterInfo, args).Run()
|
|
if err != nil {
|
|
logger.Errorf("failed to exclude osd.%d out of the crush map. %v", osdID, err)
|
|
}
|
|
|
|
// Check if we can safely remove the OSD
|
|
// Loop forever until the osd is safe-to-destroy
|
|
for {
|
|
isSafeToDestroy, err := client.OsdSafeToDestroy(clusterdContext, clusterInfo, osdID)
|
|
if err != nil {
|
|
// If we want to force remove the OSD and there was an error let's break outside of
|
|
// the loop and proceed with the OSD removal
|
|
if forceOSDRemoval {
|
|
logger.Errorf("failed to check if osd %d is safe to destroy, but force removal is enabled so proceeding with removal. %v", osdID, err)
|
|
break
|
|
} else {
|
|
logger.Errorf("failed to check if osd %d is safe to destroy, retrying in 1m. %v", osdID, err)
|
|
time.Sleep(1 * time.Minute)
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If no error and the OSD is safe to destroy, we can proceed with the OSD removal
|
|
if isSafeToDestroy {
|
|
logger.Infof("osd.%d is safe to destroy, proceeding", osdID)
|
|
break
|
|
} else {
|
|
// If we arrive here and forceOSDRemoval is true, we should proceed with the OSD removal
|
|
if forceOSDRemoval {
|
|
logger.Infof("osd.%d is NOT ok to destroy but force removal is enabled so proceeding with removal", osdID)
|
|
break
|
|
}
|
|
// Else we wait until the OSD can be removed
|
|
logger.Warningf("osd.%d is NOT ok to destroy, retrying in 15s until success", osdID)
|
|
time.Sleep(15 * time.Second)
|
|
}
|
|
}
|
|
|
|
// Remove the OSD deployment
|
|
deploymentName := fmt.Sprintf("rook-ceph-osd-%d", osdID)
|
|
deployment, err := clusterdContext.Clientset.AppsV1().Deployments(clusterInfo.Namespace).Get(clusterInfo.Context, deploymentName, metav1.GetOptions{})
|
|
if err != nil {
|
|
logger.Errorf("failed to fetch the deployment %q. %v", deploymentName, err)
|
|
} else {
|
|
logger.Infof("removing the OSD deployment %q", deploymentName)
|
|
if err := k8sutil.DeleteDeployment(clusterInfo.Context, clusterdContext.Clientset, clusterInfo.Namespace, deploymentName); err != nil {
|
|
// Continue purging the OSD even if the deployment fails to be deleted
|
|
logger.Errorf("failed to delete deployment for OSD %d. %v", osdID, err)
|
|
}
|
|
if pvcName, ok := deployment.GetLabels()[oposd.OSDOverPVCLabelKey]; ok {
|
|
removeOSDPrepareJob(clusterdContext, clusterInfo, pvcName)
|
|
removePVCs(clusterdContext, clusterInfo, pvcName, preservePVC)
|
|
} else {
|
|
logger.Infof("did not find a pvc name to remove for osd %q", deploymentName)
|
|
}
|
|
}
|
|
|
|
// purge the osd
|
|
logger.Infof("purging osd.%d", osdID)
|
|
purgeOSDArgs := []string{"osd", "purge", fmt.Sprintf("osd.%d", osdID), "--force", "--yes-i-really-mean-it"}
|
|
_, err = client.NewCephCommand(clusterdContext, clusterInfo, purgeOSDArgs).Run()
|
|
if err != nil {
|
|
logger.Errorf("failed to purge osd.%d. %v", osdID, err)
|
|
}
|
|
|
|
// Attempting to remove the parent host. Errors can be ignored if there are other OSDs on the same host
|
|
logger.Infof("attempting to remove host %q from crush map if not in use", hostName)
|
|
hostArgs := []string{"osd", "crush", "rm", hostName}
|
|
_, err = client.NewCephCommand(clusterdContext, clusterInfo, hostArgs).Run()
|
|
if err != nil {
|
|
logger.Infof("failed to remove CRUSH host %q. %v", hostName, err)
|
|
} else {
|
|
logger.Infof("removed CRUSH host %q", hostName)
|
|
}
|
|
|
|
// call archiveCrash to silence crash warning in ceph health if any
|
|
archiveCrash(clusterdContext, clusterInfo, osdID)
|
|
|
|
logger.Infof("completed removal of OSD %d", osdID)
|
|
}
|
|
|
|
func removeOSDPrepareJob(clusterdContext *clusterd.Context, clusterInfo *client.ClusterInfo, pvcName string) {
|
|
labelSelector := fmt.Sprintf("%s=%s", oposd.OSDOverPVCLabelKey, pvcName)
|
|
prepareJobList, err := clusterdContext.Clientset.BatchV1().Jobs(clusterInfo.Namespace).List(clusterInfo.Context, metav1.ListOptions{LabelSelector: labelSelector})
|
|
if err != nil && !kerrors.IsNotFound(err) {
|
|
logger.Errorf("failed to list osd prepare jobs with pvc %q. %v ", pvcName, err)
|
|
}
|
|
// Remove osd prepare job
|
|
for _, prepareJob := range prepareJobList.Items {
|
|
logger.Infof("removing the osd prepare job %q", prepareJob.GetName())
|
|
if err := k8sutil.DeleteBatchJob(clusterInfo.Context, clusterdContext.Clientset, clusterInfo.Namespace, prepareJob.GetName(), false); err != nil {
|
|
// Continue with the cleanup even if the job fails to be deleted
|
|
logger.Errorf("failed to delete prepare job for osd %q. %v", prepareJob.GetName(), err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func removePVCs(clusterdContext *clusterd.Context, clusterInfo *client.ClusterInfo, dataPVCName string, preservePVC bool) {
|
|
dataPVC, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Get(clusterInfo.Context, dataPVCName, metav1.GetOptions{})
|
|
if err != nil {
|
|
logger.Errorf("failed to get pvc for OSD %q. %v", dataPVCName, err)
|
|
return
|
|
}
|
|
labels := dataPVC.GetLabels()
|
|
deviceSet := labels[oposd.CephDeviceSetLabelKey]
|
|
setIndex := labels[oposd.CephSetIndexLabelKey]
|
|
|
|
labelSelector := fmt.Sprintf("%s=%s,%s=%s", oposd.CephDeviceSetLabelKey, deviceSet, oposd.CephSetIndexLabelKey, setIndex)
|
|
listOptions := metav1.ListOptions{LabelSelector: labelSelector}
|
|
pvcs, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).List(clusterInfo.Context, listOptions)
|
|
if err != nil {
|
|
logger.Errorf("failed to get pvcs for OSD %q. %v", dataPVCName, err)
|
|
return
|
|
}
|
|
|
|
// Delete each of the data, wal, and db PVCs that belonged to the OSD
|
|
for i, pvc := range pvcs.Items {
|
|
if preservePVC {
|
|
// Detach the OSD PVC from Rook. We will continue OSD deletion even if failed to remove PVC label
|
|
logger.Infof("detach the OSD PVC %q from Rook", pvc.Name)
|
|
delete(labels, oposd.CephDeviceSetPVCIDLabelKey)
|
|
pvc.SetLabels(labels)
|
|
if _, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Update(clusterInfo.Context, &pvcs.Items[i], metav1.UpdateOptions{}); err != nil {
|
|
logger.Errorf("failed to remove label %q from pvc for OSD %q. %v", oposd.CephDeviceSetPVCIDLabelKey, pvc.Name, err)
|
|
}
|
|
} else {
|
|
// Remove the OSD PVC
|
|
logger.Infof("removing the OSD PVC %q", pvc.Name)
|
|
if err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Delete(clusterInfo.Context, pvc.Name, metav1.DeleteOptions{}); err != nil {
|
|
// Continue deleting the OSD PVC even if PVC deletion fails
|
|
logger.Errorf("failed to delete pvc %q for OSD. %v", pvc.Name, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func archiveCrash(clusterdContext *clusterd.Context, clusterInfo *client.ClusterInfo, osdID int) {
|
|
// The ceph health warning should be silenced by archiving the crash
|
|
crash, err := client.GetCrash(clusterdContext, clusterInfo)
|
|
if err != nil {
|
|
logger.Errorf("failed to list ceph crash. %v", err)
|
|
return
|
|
}
|
|
if len(crash) == 0 {
|
|
logger.Info("no ceph crash to silence")
|
|
return
|
|
}
|
|
|
|
osdEntity := fmt.Sprintf("osd.%d", osdID)
|
|
matched := false
|
|
for _, c := range crash {
|
|
if c.Entity != osdEntity {
|
|
continue
|
|
}
|
|
matched = true
|
|
if err := client.ArchiveCrash(clusterdContext, clusterInfo, c.ID); err != nil {
|
|
logger.Errorf("failed to archive the crash %q. %v", c.ID, err)
|
|
}
|
|
}
|
|
if !matched {
|
|
logger.Infof("no ceph crash for osd.%d to silence", osdID)
|
|
}
|
|
}
|
|
|
|
// DestroyOSD fetches the OSD to be migrated based on the ID and then destroys that OSD and zaps the backing device
|
|
func DestroyOSD(context *clusterd.Context, clusterInfo *client.ClusterInfo, id int, isPVC bool) (*oposd.OSDInfo, error) {
|
|
osdInfo, err := GetOSDInfoById(context, clusterInfo, id)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get OSD info for OSD.%d", id)
|
|
}
|
|
|
|
logger.Infof("destroying osd.%d", osdInfo.ID)
|
|
destroyOSDArgs := []string{"osd", "destroy", fmt.Sprintf("osd.%d", osdInfo.ID), "--yes-i-really-mean-it"}
|
|
_, err = client.NewCephCommand(context, clusterInfo, destroyOSDArgs).Run()
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to destroy osd.%d.", osdInfo.ID)
|
|
}
|
|
logger.Infof("successfully destroyed osd.%d", osdInfo.ID)
|
|
|
|
// in case of OSD on PVs, fetch the actual device name for the mounted /mnt/<pvc-name>
|
|
if isPVC {
|
|
pvcName := os.Getenv(oposd.PVCNameEnvVarName)
|
|
|
|
// remove the dm device
|
|
if osdInfo.Encrypted {
|
|
target := oposd.EncryptionDMName(pvcName, oposd.DmcryptBlockType)
|
|
err = RemoveEncryptedDevice(context, target)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to remove dm device %q", target)
|
|
}
|
|
}
|
|
// fetch the actual device for cleanup
|
|
blockPath := fmt.Sprintf("/mnt/%s", pvcName)
|
|
diskInfo, err := clusterd.PopulateDeviceInfo(blockPath, context.Executor)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to get device info for %q", blockPath)
|
|
}
|
|
osdInfo.BlockPath = diskInfo.RealPath
|
|
}
|
|
|
|
logger.Infof("zap OSD.%d path %q", osdInfo.ID, osdInfo.BlockPath)
|
|
output, err := context.Executor.ExecuteCommandWithCombinedOutput("stdbuf", "-oL", "ceph-volume", "lvm", "zap", osdInfo.BlockPath, "--destroy")
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to zap osd.%d path %q. %s.", osdInfo.ID, osdInfo.BlockPath, output)
|
|
}
|
|
|
|
logger.Infof("%s\n", output)
|
|
logger.Infof("successfully zapped osd.%d path %q", osdInfo.ID, osdInfo.BlockPath)
|
|
|
|
return osdInfo, nil
|
|
}
|