forked from rook/rook
If multiple removal jobs are fired in parallel, there is a risk of losing data since we will forcefully remove the OSD. It's also simply true if a single OSD is not safe to destroy, there is also a risk of data loss. So now, we check if the OSD is safe-to-destroy first and then proceed. The code waits forever and retries every minute unless the --force-osd-removal flag is passed. Signed-off-by: Sébastien Han <seb@redhat.com>
223 lines
8.5 KiB
Go
223 lines
8.5 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"
|
|
"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"
|
|
"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)
|
|
}
|
|
|
|
// Check we can remove the OSD
|
|
// Loop forever until the osd is safe-to-destroy
|
|
for {
|
|
isSafeToDestroy, err := client.OsdSafeToDestroy(context, 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 be 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 be ok to destroy, retrying in 1m until success", osdID)
|
|
time.Sleep(1 * time.Minute)
|
|
}
|
|
}
|
|
|
|
removeOSD(context, clusterInfo, osdID, preservePVC)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func removeOSD(clusterdContext *clusterd.Context, clusterInfo *client.ClusterInfo, osdID int, preservePVC 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)
|
|
}
|
|
|
|
// 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 {
|
|
if 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()[osd.OSDOverPVCLabelKey]; ok {
|
|
labelSelector := fmt.Sprintf("%s=%s", osd.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 {
|
|
if 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)
|
|
}
|
|
}
|
|
}
|
|
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", pvcName)
|
|
if pvc, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Get(clusterInfo.Context, pvcName, metav1.GetOptions{}); err != nil {
|
|
logger.Errorf("failed to get pvc for OSD %q. %v", pvcName, err)
|
|
} else {
|
|
labels := pvc.GetLabels()
|
|
delete(labels, osd.CephDeviceSetPVCIDLabelKey)
|
|
pvc.SetLabels(labels)
|
|
if _, err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Update(clusterInfo.Context, pvc, metav1.UpdateOptions{}); err != nil {
|
|
logger.Errorf("failed to remove label %q from pvc for OSD %q. %v", osd.CephDeviceSetPVCIDLabelKey, pvcName, err)
|
|
}
|
|
}
|
|
} else {
|
|
// Remove the OSD PVC
|
|
logger.Infof("removing the OSD PVC %q", pvcName)
|
|
if err := clusterdContext.Clientset.CoreV1().PersistentVolumeClaims(clusterInfo.Namespace).Delete(clusterInfo.Context, pvcName, metav1.DeleteOptions{}); err != nil {
|
|
if err != nil {
|
|
// Continue deleting the OSD PVC even if PVC deletion fails
|
|
logger.Errorf("failed to delete pvc for OSD %q. %v", pvcName, err)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
logger.Infof("did not find a pvc name to remove for osd %q", deploymentName)
|
|
}
|
|
}
|
|
|
|
// purge the osd
|
|
logger.Infof("destroying osd.%d", osdID)
|
|
purgeOSDArgs := []string{"osd", "destroy", fmt.Sprintf("osd.%d", osdID), "--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("removing osd.%d from ceph", osdID)
|
|
hostArgs := []string{"osd", "crush", "rm", hostName}
|
|
_, err = client.NewCephCommand(clusterdContext, clusterInfo, hostArgs).Run()
|
|
if err != nil {
|
|
logger.Errorf("failed to remove CRUSH host %q. %v", hostName, err)
|
|
}
|
|
|
|
// call archiveCrash to silence crash warning in ceph health if any
|
|
archiveCrash(clusterdContext, clusterInfo, osdID)
|
|
|
|
logger.Infof("completed removal of OSD %d", osdID)
|
|
}
|
|
|
|
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 crash != nil {
|
|
logger.Info("no ceph crash to silence")
|
|
return
|
|
}
|
|
|
|
var crashID string
|
|
for _, c := range crash {
|
|
if c.Entity == fmt.Sprintf("osd.%d", osdID) {
|
|
crashID = c.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
err = client.ArchiveCrash(clusterdContext, clusterInfo, crashID)
|
|
if err != nil {
|
|
logger.Errorf("failed to archive the crash %q. %v", crashID, err)
|
|
}
|
|
}
|