forked from rook/rook
deleting an erasure coded CephBlockPool left the ec profile orphaned. recreating with different settings failed because the old profile could not be overridden. - delete ec profile during CephBlockPool deletion - disable --format json on ec profile set command - fix ec profile name mismatch in CephObjectStore deletion Signed-off-by: Oded Viner <oviner@redhat.com>
781 lines
33 KiB
Go
781 lines
33 KiB
Go
/*
|
|
Copyright 2016 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 pool to manage a rook pool.
|
|
package pool
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/coreos/pkg/capnslog"
|
|
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
|
|
"github.com/rook/rook/pkg/util/dependents"
|
|
"github.com/rook/rook/pkg/util/exec"
|
|
"github.com/rook/rook/pkg/util/log"
|
|
|
|
"github.com/pkg/errors"
|
|
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
|
|
"github.com/rook/rook/pkg/clusterd"
|
|
"github.com/rook/rook/pkg/operator/ceph/cluster/mon"
|
|
"github.com/rook/rook/pkg/operator/ceph/config"
|
|
opcontroller "github.com/rook/rook/pkg/operator/ceph/controller"
|
|
"github.com/rook/rook/pkg/operator/ceph/csi/peermap"
|
|
"github.com/rook/rook/pkg/operator/ceph/reporting"
|
|
"github.com/rook/rook/pkg/operator/k8sutil"
|
|
corev1 "k8s.io/api/core/v1"
|
|
kerrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/events"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/controller"
|
|
"sigs.k8s.io/controller-runtime/pkg/handler"
|
|
"sigs.k8s.io/controller-runtime/pkg/manager"
|
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
|
"sigs.k8s.io/controller-runtime/pkg/source"
|
|
)
|
|
|
|
const (
|
|
poolApplicationNameRBD = "rbd"
|
|
controllerName = "ceph-block-pool-controller"
|
|
)
|
|
|
|
var logger = capnslog.NewPackageLogger("github.com/rook/rook", controllerName)
|
|
|
|
// Sets the type meta for the controller main object
|
|
var controllerTypeMeta = metav1.TypeMeta{
|
|
Kind: reflect.TypeFor[cephv1.CephBlockPool]().Name(),
|
|
APIVersion: fmt.Sprintf("%s/%s", cephv1.CustomResourceGroup, cephv1.Version),
|
|
}
|
|
|
|
var _ reconcile.Reconciler = &ReconcileCephBlockPool{}
|
|
|
|
// ReconcileCephBlockPool reconciles a CephBlockPool object
|
|
type ReconcileCephBlockPool struct {
|
|
client client.Client
|
|
scheme *runtime.Scheme
|
|
context *clusterd.Context
|
|
clusterInfo *cephclient.ClusterInfo
|
|
blockPoolMirrorContexts map[string]*blockPoolHealth
|
|
opManagerContext context.Context
|
|
recorder events.EventRecorder
|
|
opConfig opcontroller.OperatorConfig
|
|
}
|
|
|
|
type blockPoolHealth struct {
|
|
internalCtx context.Context
|
|
internalCancel context.CancelFunc
|
|
started bool
|
|
}
|
|
|
|
// Add creates a new CephBlockPool Controller and adds it to the Manager. The Manager will set fields on the Controller
|
|
// and Start it when the Manager is Started.
|
|
func Add(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) error {
|
|
return add(opManagerContext, mgr, newReconciler(mgr, context, opManagerContext, opConfig))
|
|
}
|
|
|
|
// newReconciler returns a new reconcile.Reconciler
|
|
func newReconciler(mgr manager.Manager, context *clusterd.Context, opManagerContext context.Context, opConfig opcontroller.OperatorConfig) reconcile.Reconciler {
|
|
return &ReconcileCephBlockPool{
|
|
client: mgr.GetClient(),
|
|
scheme: mgr.GetScheme(),
|
|
context: context,
|
|
blockPoolMirrorContexts: make(map[string]*blockPoolHealth),
|
|
opManagerContext: opManagerContext,
|
|
recorder: mgr.GetEventRecorder("rook-" + controllerName),
|
|
opConfig: opConfig,
|
|
}
|
|
}
|
|
|
|
func add(opManagerContext context.Context, mgr manager.Manager, r reconcile.Reconciler) error {
|
|
// Create a new controller
|
|
c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: r})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Info("successfully started")
|
|
|
|
// Watch for changes on the CephBlockPool CRD object
|
|
err = c.Watch(
|
|
source.Kind(
|
|
mgr.GetCache(),
|
|
&cephv1.CephBlockPool{TypeMeta: controllerTypeMeta},
|
|
&handler.TypedEnqueueRequestForObject[*cephv1.CephBlockPool]{},
|
|
opcontroller.WatchControllerPredicate[*cephv1.CephBlockPool](mgr.GetScheme()),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build Handler function to return the list of ceph block pool
|
|
// This is used by the watchers below
|
|
configHandler, err := opcontroller.ObjectToCRMapper[*cephv1.CephBlockPoolList, *corev1.ConfigMap](
|
|
opManagerContext,
|
|
mgr.GetClient(),
|
|
&cephv1.CephBlockPoolList{},
|
|
mgr.GetScheme(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Watch for ConfigMap "rook-ceph-mon-endpoints" update and reconcile, which will reconcile update the bootstrap peer token
|
|
err = c.Watch(
|
|
source.Kind(
|
|
mgr.GetCache(),
|
|
&corev1.ConfigMap{TypeMeta: metav1.TypeMeta{Kind: "ConfigMap", APIVersion: corev1.SchemeGroupVersion.String()}},
|
|
handler.TypedEnqueueRequestsFromMapFunc(configHandler),
|
|
mon.PredicateMonEndpointChanges(),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build Handler function to return the list of ceph block pool
|
|
// This is used by the watchers below
|
|
secretHandler, err := opcontroller.ObjectToCRMapper[*cephv1.CephBlockPoolList, *corev1.Secret](
|
|
opManagerContext,
|
|
mgr.GetClient(),
|
|
&cephv1.CephBlockPoolList{},
|
|
mgr.GetScheme(),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Watch for updates to the secret triggered by changes in the peer pool token
|
|
err = c.Watch(
|
|
source.Kind(
|
|
mgr.GetCache(),
|
|
&corev1.Secret{TypeMeta: metav1.TypeMeta{Kind: "Secret", APIVersion: corev1.SchemeGroupVersion.String()}},
|
|
handler.TypedEnqueueRequestsFromMapFunc(secretHandler),
|
|
opcontroller.WatchPeerTokenSecretPredicate(),
|
|
),
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reconcile reads that state of the cluster for a CephBlockPool object and makes changes based on the state read
|
|
// and what is in the CephBlockPool.Spec
|
|
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
|
|
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
|
|
func (r *ReconcileCephBlockPool) Reconcile(context context.Context, request reconcile.Request) (reconcile.Result, error) {
|
|
defer opcontroller.RecoverAndLogException()
|
|
// workaround because the rook logging mechanism is not compatible with the controller-runtime logging interface
|
|
reconcileResponse, cephBlockPool, err := r.reconcile(request)
|
|
|
|
return reporting.ReportReconcileResult(logger, r.recorder, request, &cephBlockPool, reconcileResponse, err)
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) reconcile(request reconcile.Request) (reconcile.Result, cephv1.CephBlockPool, error) {
|
|
// Fetch the CephBlockPool instance
|
|
cephBlockPool := &cephv1.CephBlockPool{}
|
|
err := r.client.Get(r.opManagerContext, request.NamespacedName, cephBlockPool)
|
|
if err != nil {
|
|
if kerrors.IsNotFound(err) {
|
|
log.NamedDebug(request.NamespacedName, logger, "CephBlockPool resource not found. Ignoring since object must be deleted.")
|
|
// If there was a previous error or if a user removed this resource's finalizer, it's
|
|
// possible Rook didn't clean up the monitoring routine for this resource. Ensure the
|
|
// routine is stopped when we see the resource is gone.
|
|
cephBlockPool.Name = request.Name
|
|
cephBlockPool.Namespace = request.Namespace
|
|
r.cancelMirrorMonitoring(cephBlockPool)
|
|
return reconcile.Result{}, *cephBlockPool, nil
|
|
}
|
|
// Error reading the object - requeue the request.
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrap(err, "failed to get CephBlockPool")
|
|
}
|
|
// update observedGeneration local variable with current generation value,
|
|
// because generation can be changed before reconcile got completed
|
|
// CR status will be updated at end of reconcile, so to reflect the reconcile has finished
|
|
observedGeneration := cephBlockPool.ObjectMeta.Generation
|
|
|
|
// Set a finalizer so we can do cleanup before the object goes away
|
|
generationUpdated, err := opcontroller.AddFinalizerIfNotPresent(r.opManagerContext, r.client, cephBlockPool)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrap(err, "failed to add finalizer")
|
|
}
|
|
if generationUpdated {
|
|
log.NamedInfo(request.NamespacedName, logger, "reconciling the ceph block pool after adding finalizer")
|
|
return reconcile.Result{}, *cephBlockPool, nil
|
|
}
|
|
|
|
var statusErr error
|
|
// The CR was just created, initializing status fields
|
|
if cephBlockPool.Status == nil {
|
|
// The pool is not available so let's not build the status Info yet
|
|
err = r.updateStatus(request.NamespacedName, cephv1.ConditionProgressing, k8sutil.ObservedGenerationNotAvailable, &cephv1.CephxStatus{})
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "failed to update %q status to %q", request.NamespacedName, cephv1.ConditionProgressing)
|
|
}
|
|
}
|
|
|
|
// Make sure a CephCluster is present otherwise do nothing
|
|
cephCluster, isReadyToReconcile, cephClusterExists, reconcileResponse := opcontroller.IsReadyToReconcile(r.opManagerContext, r.client, request.NamespacedName, controllerName)
|
|
if !isReadyToReconcile {
|
|
// This handles the case where the Ceph Cluster is gone and we want to delete that CR
|
|
// We skip the deletePool() function since everything is gone already
|
|
//
|
|
// Also, only remove the finalizer if the CephCluster is gone
|
|
// If not, we should wait for it to be ready
|
|
// This handles the case where the operator is not ready to accept Ceph command but the cluster exists
|
|
if !cephBlockPool.GetDeletionTimestamp().IsZero() && !cephClusterExists {
|
|
// don't leak the health checker routine if we are force-deleting
|
|
r.cancelMirrorMonitoring(cephBlockPool)
|
|
|
|
// Remove finalizer
|
|
err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephBlockPool)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrap(err, "failed to remove finalizer")
|
|
}
|
|
|
|
// Return and do not requeue. Successful deletion.
|
|
return reconcile.Result{}, *cephBlockPool, nil
|
|
}
|
|
return reconcileResponse, *cephBlockPool, nil
|
|
}
|
|
|
|
// Populate clusterInfo during each reconcile
|
|
clusterInfo, _, _, err := opcontroller.LoadClusterInfo(r.context, r.opManagerContext, request.NamespacedName.Namespace, &cephCluster.Spec)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrap(err, "failed to populate cluster info")
|
|
}
|
|
r.clusterInfo = clusterInfo
|
|
|
|
poolSpec := cephBlockPool.ToNamedPoolSpec()
|
|
// DELETE: the CR was deleted
|
|
if !cephBlockPool.GetDeletionTimestamp().IsZero() {
|
|
if err := r.handleDeletionBlocked(cephBlockPool, &cephCluster); err != nil {
|
|
return opcontroller.WaitForRequeueIfFinalizerBlocked, *cephBlockPool, err
|
|
}
|
|
|
|
// If the ceph block pool is still in the map, we must remove it during CR deletion
|
|
// We must remove it first otherwise the checker will panic since the status/info will be nil
|
|
r.cancelMirrorMonitoring(cephBlockPool)
|
|
|
|
r.recorder.Eventf(cephBlockPool, nil, corev1.EventTypeNormal, string(cephv1.ReconcileStarted), string(cephv1.ReconcileStarted), "starting blockpool deletion")
|
|
|
|
log.NamedInfo(request.NamespacedName, logger, "deleting pool")
|
|
err = deletePool(r.context, clusterInfo, &poolSpec)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "failed to delete pool %q. ", cephBlockPool.Name)
|
|
}
|
|
|
|
// disable RBD stats collection if cephBlockPool was deleted
|
|
if err := configureRBDStats(r.context, clusterInfo, cephBlockPool.Name); err != nil {
|
|
log.NamedError(request.NamespacedName, logger, "failed to disable stats collection for pool(s). %v", err)
|
|
}
|
|
|
|
// Remove finalizer
|
|
err = opcontroller.RemoveFinalizer(r.opManagerContext, r.client, cephBlockPool)
|
|
if err != nil {
|
|
r.recorder.Eventf(cephBlockPool, nil, corev1.EventTypeWarning, string(cephv1.ReconcileFailed), string(cephv1.ReconcileFailed), "failed to remove finalizer")
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrap(err, "failed to remove finalizer")
|
|
}
|
|
|
|
r.recorder.Eventf(cephBlockPool, nil, corev1.EventTypeNormal, string(cephv1.ReconcileSucceeded), string(cephv1.ReconcileSucceeded), "successfully removed finalizer")
|
|
|
|
// Return and do not requeue. Successful deletion.
|
|
return reconcile.Result{}, *cephBlockPool, nil
|
|
}
|
|
|
|
// validate the pool settings
|
|
if err := validatePool(r.context, clusterInfo, &cephCluster.Spec, cephBlockPool); err != nil {
|
|
if strings.Contains(err.Error(), opcontroller.UninitializedCephConfigError) {
|
|
log.NamedInfo(request.NamespacedName, logger, opcontroller.OperatorNotInitializedMessage)
|
|
return opcontroller.WaitForRequeueIfOperatorNotInitialized, *cephBlockPool, nil
|
|
}
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "invalid pool CR %q spec", cephBlockPool.Name)
|
|
}
|
|
|
|
// Get CephCluster version
|
|
cephVersion, err := opcontroller.GetImageVersion(cephCluster)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(err, "failed to fetch ceph version from cephcluster %q", cephCluster.Name)
|
|
}
|
|
r.clusterInfo.CephVersion = *cephVersion
|
|
|
|
// CREATE/UPDATE
|
|
reconcileResponse, err = r.reconcileCreatePool(clusterInfo, &cephCluster.Spec, cephBlockPool)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), opcontroller.UninitializedCephConfigError) {
|
|
log.NamedInfo(request.NamespacedName, logger, opcontroller.OperatorNotInitializedMessage)
|
|
return opcontroller.WaitForRequeueIfOperatorNotInitialized, *cephBlockPool, nil
|
|
}
|
|
statusErr = r.updateStatus(request.NamespacedName, cephv1.ConditionFailure, k8sutil.ObservedGenerationNotAvailable, nil)
|
|
if statusErr != nil {
|
|
log.NamedError(request.NamespacedName, logger, "failed to update status to %q: %v", cephv1.ConditionFailure, statusErr)
|
|
}
|
|
return reconcileResponse, *cephBlockPool, errors.Wrapf(err, "failed to create pool %q.", cephBlockPool.GetName())
|
|
}
|
|
|
|
// enable/disable RBD stats collection based on cephBlockPool spec
|
|
if err := configureRBDStats(r.context, clusterInfo, ""); err != nil {
|
|
return reconcile.Result{}, *cephBlockPool, errors.Wrap(err, "failed to enable/disable stats collection for pool(s)")
|
|
}
|
|
|
|
if canConfigurePoolMirroring(poolSpec) {
|
|
var reconcileResult reconcile.Result
|
|
reconcileResult, statusErr, err = r.configurePoolMirroring(request, poolSpec, cephBlockPool, clusterInfo, observedGeneration, cephCluster)
|
|
if err != nil {
|
|
return reconcileResult, *cephBlockPool, err
|
|
}
|
|
} else {
|
|
statusErr = r.updateStatus(request.NamespacedName, cephv1.ConditionReady, observedGeneration, nil)
|
|
}
|
|
|
|
if statusErr != nil {
|
|
return opcontroller.ImmediateRetryResult, *cephBlockPool, errors.Wrapf(statusErr, "failed to update status of pool %q to %q.", cephBlockPool.Name, cephv1.ConditionReady)
|
|
}
|
|
|
|
// Return and do not requeue
|
|
log.NamedDebug(request.NamespacedName, logger, "done reconciling")
|
|
return reconcile.Result{}, *cephBlockPool, nil
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) configurePoolMirroring(request reconcile.Request, poolSpec cephv1.NamedPoolSpec, cephBlockPool *cephv1.CephBlockPool, clusterInfo *cephclient.ClusterInfo, observedGeneration int64, cephCluster cephv1.CephCluster) (reconcile.Result, error, error) {
|
|
checker := cephclient.NewMirrorChecker(r.context, r.client, r.clusterInfo, request.NamespacedName, &poolSpec, cephBlockPool)
|
|
// ADD PEERS
|
|
log.NamedDebug(request.NamespacedName, logger, "reconciling create rbd mirror peer configuration")
|
|
|
|
// Initialize the channel for this pool
|
|
// This allows us to track multiple CephBlockPool in the same namespace
|
|
blockPoolChannelKey := blockPoolChannelKeyName(cephBlockPool)
|
|
_, blockPoolMirrorContextsExists := r.blockPoolMirrorContexts[blockPoolChannelKey]
|
|
if !blockPoolMirrorContextsExists {
|
|
internalCtx, internalCancel := context.WithCancel(r.opManagerContext)
|
|
r.blockPoolMirrorContexts[blockPoolChannelKey] = &blockPoolHealth{
|
|
internalCtx: internalCtx,
|
|
internalCancel: internalCancel,
|
|
}
|
|
}
|
|
var statusErr error
|
|
|
|
if cephBlockPool.Spec.Mirroring.Enabled {
|
|
// Always create a bootstrap peer token in case another cluster wants to add us as a peer
|
|
reconcileResponse, err := opcontroller.CreateBootstrapPeerSecret(r.context, clusterInfo, cephBlockPool, k8sutil.NewOwnerInfo(cephBlockPool, r.scheme))
|
|
if err != nil {
|
|
statusErr := r.updateStatus(request.NamespacedName, cephv1.ConditionFailure, k8sutil.ObservedGenerationNotAvailable, nil)
|
|
if statusErr != nil {
|
|
return opcontroller.ImmediateRetryResult, statusErr, errors.Wrapf(statusErr, "failed to update %q status to %q", request.NamespacedName, cephv1.ConditionFailure)
|
|
}
|
|
return reconcileResponse, statusErr, errors.Wrapf(err, "failed to create rbd-mirror bootstrap peer for pool %q.", cephBlockPool.GetName())
|
|
}
|
|
|
|
// update rbdMirror cephXStatus immediately after bootstrapping the peer token
|
|
statusErr = r.updateStatus(request.NamespacedName, cephv1.ConditionProgressing, observedGeneration, &cephCluster.Status.Cephx.RBDMirrorPeer)
|
|
if statusErr != nil {
|
|
return opcontroller.ImmediateRetryResult, statusErr, errors.Wrapf(statusErr, "failed to update %q status to %q", request.NamespacedName, cephv1.ConditionProgressing)
|
|
}
|
|
|
|
// Check if rbd-mirror CR and daemons are running
|
|
log.NamedDebug(request.NamespacedName, logger, "listing rbd-mirror CR")
|
|
|
|
// Add bootstrap peer if any
|
|
log.NamedDebug(request.NamespacedName, logger, "reconciling ceph bootstrap peers import")
|
|
reconcileResponse, err = r.reconcileAddBootstrapPeer(cephBlockPool, request.NamespacedName)
|
|
if err != nil {
|
|
return reconcileResponse, statusErr, errors.Wrap(err, "failed to add ceph rbd mirror peer")
|
|
}
|
|
|
|
// ReconcilePoolIDMap updates the `rook-ceph-csi-mapping-config` with local and peer cluster pool ID map
|
|
err = peermap.ReconcilePoolIDMap(r.opManagerContext, r.context, r.clusterInfo, cephBlockPool)
|
|
if err != nil {
|
|
return reconcileResponse, statusErr, errors.Wrapf(err, "failed to update pool ID mapping config for the pool %q", cephBlockPool.Name)
|
|
}
|
|
|
|
// update ObservedGeneration in status at the end of reconcile
|
|
// Set Ready status, we are done reconciling
|
|
statusErr = r.updateStatus(request.NamespacedName, cephv1.ConditionReady, observedGeneration, nil)
|
|
|
|
if cephBlockPool.Spec.StatusCheck.Mirror.Disabled {
|
|
// Stop monitoring the mirroring status of this pool
|
|
if blockPoolMirrorContextsExists && r.blockPoolMirrorContexts[blockPoolChannelKey].started {
|
|
log.NamedInfo(request.NamespacedName, logger, "stop monitoring the mirroring status of the pool")
|
|
r.cancelMirrorMonitoring(cephBlockPool)
|
|
}
|
|
// Reset the MirrorHealthCheckSpec
|
|
checker.UpdateStatusMirroring(nil, nil, nil, "")
|
|
} else {
|
|
// Start monitoring of the pool
|
|
if r.blockPoolMirrorContexts[blockPoolChannelKey].started {
|
|
log.NamedDebug(request.NamespacedName, logger, "pool monitoring go routine already running!")
|
|
} else {
|
|
if cephBlockPool.Spec.Mirroring.Mode != "init-only" {
|
|
r.blockPoolMirrorContexts[blockPoolChannelKey].started = true
|
|
// Run the goroutine to update the mirroring status and skip when blockpool mirroing mode in init-only as radosnamespace mirroring is the right place to check
|
|
// mirroring status when blockpool mirroring mode is init-only.
|
|
go checker.CheckMirroring(r.blockPoolMirrorContexts[blockPoolChannelKey].internalCtx)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If not mirrored there is no Status Info field to fulfil
|
|
} else {
|
|
// disable mirroring
|
|
err := r.disableMirroring(poolSpec.Name)
|
|
if err != nil {
|
|
log.NamedWarning(request.NamespacedName, logger, "failed to disable mirroring on pool. %v", err)
|
|
}
|
|
// update ObservedGeneration in status at the end of reconcile
|
|
// Set Ready status, we are done reconciling
|
|
statusErr = r.updateStatus(request.NamespacedName, cephv1.ConditionReady, observedGeneration, &cephv1.CephxStatus{})
|
|
|
|
// Stop monitoring the mirroring status of this pool
|
|
if blockPoolMirrorContextsExists && r.blockPoolMirrorContexts[blockPoolChannelKey].started {
|
|
r.cancelMirrorMonitoring(cephBlockPool)
|
|
}
|
|
// Reset the MirrorHealthCheckSpec
|
|
checker.UpdateStatusMirroring(nil, nil, nil, "")
|
|
}
|
|
|
|
return reconcile.Result{}, statusErr, nil
|
|
}
|
|
|
|
// handlePoolDeletionBlocked updates the blockpool CR status with conditions about
|
|
// whether the pool is empty or has dependents that block deletion.
|
|
// If the pool is not empty and force deletion is specified, create a cleanup job
|
|
// to delete the images and snapshots forcefully.
|
|
func (r *ReconcileCephBlockPool) handleDeletionBlocked(cephBlockPool *cephv1.CephBlockPool, cephCluster *cephv1.CephCluster) error {
|
|
nsName := opcontroller.NsName(cephBlockPool.Namespace, cephBlockPool.Name)
|
|
poolSpec := cephBlockPool.ToNamedPoolSpec()
|
|
deletionBlocked := false
|
|
|
|
deps, err := cephBlockPoolDependents(r.context, r.clusterInfo, cephBlockPool)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var depCondition cephv1.Condition
|
|
if deps.Empty() {
|
|
_, _, depCondition = reporting.GenerateConditionUnblockedDueToDependents(cephBlockPool)
|
|
} else {
|
|
deletionBlocked = true
|
|
_, _, depCondition = reporting.GenerateConditionBlockedDueToDependents(cephBlockPool, deps)
|
|
}
|
|
log.NamedInfo(nsName, logger, "%s", depCondition.Message)
|
|
|
|
radosNamespaces := deps.OfKind(radosNamespacesKeyName)
|
|
poolPresent, err := cephclient.IsPoolPresent(r.context, r.clusterInfo, poolSpec.Name)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to check pool presence")
|
|
}
|
|
var isEmpty bool
|
|
var emptyMessage string
|
|
if poolPresent {
|
|
isEmpty, emptyMessage, err = cephclient.IsPoolEmpty(r.context, r.clusterInfo, poolSpec.Name, radosNamespaces)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
emptyMessage = fmt.Sprintf("pool %q is not found in cluster", poolSpec.Name)
|
|
}
|
|
var emptyCondition cephv1.Condition
|
|
if isEmpty || !poolPresent {
|
|
emptyCondition = dependents.DeletionBlockedDueToNonEmptyPoolCondition(false, emptyMessage)
|
|
} else {
|
|
deletionBlocked = true
|
|
emptyCondition = dependents.DeletionBlockedDueToNonEmptyPoolCondition(true, emptyMessage)
|
|
}
|
|
log.NamedInfo(nsName, logger, "%s", emptyCondition.Message)
|
|
|
|
err = reporting.UpdateStatusConditionsWithRetry(
|
|
r.opManagerContext, r.client, cephBlockPool, nsName, cephBlockPool.Kind, emptyCondition, depCondition)
|
|
if err != nil {
|
|
log.NamedWarning(nsName, logger, "failed to update %q status with deletion blocked conditions: %v", nsName.String(), err)
|
|
}
|
|
|
|
if !isEmpty {
|
|
// Force deletion if desired
|
|
if opcontroller.ForceDeleteRequested(cephBlockPool.GetAnnotations()) {
|
|
cleanupErr := r.cleanup(cephBlockPool, cephCluster)
|
|
if cleanupErr != nil {
|
|
return errors.Wrapf(cleanupErr, "failed to create clean up job for ceph blockpool %q", cephBlockPool.Name)
|
|
}
|
|
}
|
|
}
|
|
|
|
if deletionBlocked {
|
|
return errors.Errorf("pool %q cannot be deleted because it is not empty or has dependents", cephBlockPool.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) reconcileCreatePool(clusterInfo *cephclient.ClusterInfo, cephCluster *cephv1.ClusterSpec, cephBlockPool *cephv1.CephBlockPool) (reconcile.Result, error) {
|
|
poolSpec := cephBlockPool.ToNamedPoolSpec()
|
|
err := createPool(r.context, clusterInfo, cephCluster, &poolSpec)
|
|
if err != nil {
|
|
return opcontroller.ImmediateRetryResult, errors.Wrapf(err, "failed to configure pool %q.", cephBlockPool.GetName())
|
|
}
|
|
|
|
// Let's return here so that on the initial creation we don't check for update right away
|
|
return reconcile.Result{}, nil
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) cleanup(cephblockpool *cephv1.CephBlockPool, cephCluster *cephv1.CephCluster) error {
|
|
nsName := opcontroller.NsName(cephblockpool.Namespace, cephblockpool.Name)
|
|
log.NamedInfo(nsName, logger, "starting cleanup of the ceph resources for CephBlockPool")
|
|
cleanupConfig := map[string]string{
|
|
opcontroller.CephBlockPoolNameEnv: cephblockpool.Name,
|
|
}
|
|
cleanup := opcontroller.NewResourceCleanup(cephblockpool, cephCluster, r.opConfig.Image, cleanupConfig)
|
|
jobName := k8sutil.TruncateNodeNameForJob("cleanup-cephblockpool-%s", cephblockpool.Name)
|
|
err := cleanup.StartJob(r.clusterInfo.Context, r.context.Clientset, jobName)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to run clean up job to clean the cephblockpool %q", cephblockpool.Name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Create the pool
|
|
func createPool(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo, clusterSpec *cephv1.ClusterSpec, p *cephv1.NamedPoolSpec) error {
|
|
nsName := opcontroller.NsName(clusterInfo.Namespace, p.Name)
|
|
// Set the application name to rbd by default, but override later for special pools
|
|
if p.Application == "" {
|
|
p.Application = poolApplicationNameRBD
|
|
}
|
|
// create the pool
|
|
log.NamedInfo(nsName, logger, "creating pool")
|
|
if err := cephclient.CreatePool(context, clusterInfo, clusterSpec, p); err != nil {
|
|
return errors.Wrapf(err, "failed to configure pool %q", p.Name)
|
|
}
|
|
|
|
if p.Application != poolApplicationNameRBD {
|
|
return nil
|
|
}
|
|
log.NamedInfo(nsName, logger, "initializing pool for RBD use")
|
|
args := []string{"pool", "init", p.Name}
|
|
output, err := cephclient.NewRBDCommand(context, clusterInfo, args).RunWithTimeout(exec.CephCommandsTimeout)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to initialize pool %q for RBD use. %s", p.Name, string(output))
|
|
}
|
|
log.NamedInfo(nsName, logger, "successfully initialized pool for RBD use")
|
|
|
|
return nil
|
|
}
|
|
|
|
// Delete the pool
|
|
func deletePool(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo, p *cephv1.NamedPoolSpec) error {
|
|
poolPresent, err := cephclient.IsPoolPresent(context, clusterInfo, p.Name)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to check pool presence")
|
|
}
|
|
if poolPresent {
|
|
err := cephclient.DeletePool(context, clusterInfo, p.Name)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to delete pool %q", p.Name)
|
|
}
|
|
}
|
|
|
|
// Clean up the erasure code profile for EC pools so that recreating with
|
|
// different settings does not fail
|
|
if p.IsErasureCoded() {
|
|
ecProfileName := cephclient.GetErasureCodeProfileForPool(p.Name)
|
|
if err := cephclient.DeleteErasureCodeProfile(context, clusterInfo, ecProfileName); err != nil {
|
|
logger.Warningf("failed to delete erasure code profile %q for pool %q: %v", ecProfileName, p.Name, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// generateStatsPoolList combines existingStatsPools and rookStatsPools, removes items in removePools,
|
|
// removes duplicates, ensures no empty strings, and returns a comma-separated string in a deterministic order.
|
|
func generateStatsPoolList(existingStatsPools []string, rookStatsPools []string, removePools []string) string {
|
|
poolList := []string{}
|
|
|
|
// Helper function to add a poolList if it's not in the removePools list and not already in poolList
|
|
addUniquePool := func(pool string) {
|
|
if pool == "" {
|
|
return
|
|
}
|
|
// Check if the pool should be removed or already exists in poolList
|
|
if slices.Contains(removePools, pool) || slices.Contains(poolList, pool) {
|
|
return
|
|
}
|
|
poolList = append(poolList, pool)
|
|
}
|
|
for _, pool := range existingStatsPools {
|
|
addUniquePool(pool)
|
|
}
|
|
for _, pool := range rookStatsPools {
|
|
addUniquePool(pool)
|
|
}
|
|
|
|
sort.Strings(poolList) // Sort the list to ensure deterministic output
|
|
|
|
return strings.Join(poolList, ",")
|
|
}
|
|
|
|
func configureRBDStats(clusterContext *clusterd.Context, clusterInfo *cephclient.ClusterInfo, deletedPool string) error {
|
|
nsName := opcontroller.NsName(clusterInfo.Namespace, deletedPool)
|
|
log.NamedDebug(nsName, logger, "configuring RBD per-image IO statistics collection")
|
|
namespaceListOpt := client.InNamespace(clusterInfo.Namespace)
|
|
cephBlockPoolList := &cephv1.CephBlockPoolList{}
|
|
var rookStatsPools []string
|
|
var removePools []string
|
|
|
|
err := clusterContext.Client.List(clusterInfo.Context, cephBlockPoolList, namespaceListOpt)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to retrieve list of CephBlockPool")
|
|
}
|
|
for _, cephBlockPool := range cephBlockPoolList.Items {
|
|
if cephBlockPool.GetDeletionTimestamp() == nil && cephBlockPool.Spec.EnableRBDStats {
|
|
// add to list of CephBlockPool with enableRBDStats set to true and not marked for deletion
|
|
rookStatsPools = append(rookStatsPools, cephBlockPool.ToNamedPoolSpec().Name)
|
|
} else {
|
|
removePools = append(removePools, cephBlockPool.ToNamedPoolSpec().Name)
|
|
}
|
|
}
|
|
if deletedPool != "" {
|
|
removePools = append(removePools, deletedPool)
|
|
}
|
|
monStore := config.GetMonStore(clusterContext, clusterInfo)
|
|
// Check for existing rbd stats pools
|
|
existingStatsPools, e := monStore.Get("mgr", "mgr/prometheus/rbd_stats_pools")
|
|
if e != nil {
|
|
return errors.Wrapf(e, "failed to get rbd_stats_pools")
|
|
}
|
|
existingStatsPoolsList := strings.Split(existingStatsPools, ",")
|
|
enableStatsForPools := generateStatsPoolList(existingStatsPoolsList, rookStatsPools, removePools)
|
|
log.NamedDebug(nsName, logger, "RBD per-image IO statistics will be collected for pools: %v", enableStatsForPools)
|
|
if len(enableStatsForPools) == 0 {
|
|
err = monStore.Delete("mgr", "mgr/prometheus/rbd_stats_pools")
|
|
} else {
|
|
// appending existing rbd stats pools if any
|
|
err = monStore.Set("mgr", "mgr/prometheus/rbd_stats_pools", enableStatsForPools)
|
|
}
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to enable rbd_stats_pools")
|
|
}
|
|
log.NamedDebug(nsName, logger, "configured RBD per-image IO statistics collection")
|
|
return nil
|
|
}
|
|
|
|
func blockPoolChannelKeyName(p *cephv1.CephBlockPool) string {
|
|
return types.NamespacedName{Namespace: p.Namespace, Name: p.Name}.String()
|
|
}
|
|
|
|
// cancel mirror monitoring. This is a noop if monitoring is not running.
|
|
func (r *ReconcileCephBlockPool) cancelMirrorMonitoring(cephBlockPool *cephv1.CephBlockPool) {
|
|
channelKey := blockPoolChannelKeyName(cephBlockPool)
|
|
|
|
_, poolContextExists := r.blockPoolMirrorContexts[channelKey]
|
|
if poolContextExists {
|
|
// Cancel the context to stop the go routine
|
|
r.blockPoolMirrorContexts[channelKey].internalCancel()
|
|
|
|
// Remove ceph block pool from the map
|
|
delete(r.blockPoolMirrorContexts, channelKey)
|
|
}
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) disableMirroring(pool string) error {
|
|
nsName := opcontroller.NsName(r.clusterInfo.Namespace, pool)
|
|
mirrorInfo, err := cephclient.GetPoolMirroringInfo(r.context, r.clusterInfo, pool)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to get mirroring info for the pool %q", pool)
|
|
}
|
|
if mirrorInfo.Mode == "disabled" {
|
|
return nil
|
|
}
|
|
|
|
mirroringEnabled, err := r.isAnyRadosNamespaceMirrored(pool)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to check if any rados namespace is mirrored")
|
|
}
|
|
if mirroringEnabled {
|
|
log.NamedDebug(nsName, logger, "disabling mirroring on pool is not possible. There are mirrored rados namespaces in the pool")
|
|
return errors.New("mirroring must be disabled in all radosnamespaces in the pool before disabling mirroring in the pool")
|
|
}
|
|
|
|
if mirrorInfo.Mode == "image" {
|
|
mirroredPools, err := cephclient.GetMirroredPoolImages(r.context, r.clusterInfo, pool)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to list mirrored images for pool %q", pool)
|
|
}
|
|
|
|
if len(*mirroredPools.Images) > 0 {
|
|
msg := fmt.Sprintf("there are images in the pool %q. Please manually disable mirroring for each image", pool)
|
|
log.NamedError(nsName, logger, "%s", msg)
|
|
return errors.New(msg)
|
|
}
|
|
}
|
|
|
|
// Remove storage cluster peers
|
|
for _, peer := range mirrorInfo.Peers {
|
|
if peer.UUID != "" {
|
|
err := cephclient.RemoveClusterPeer(r.context, r.clusterInfo, pool, peer.UUID)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to remove cluster peer with UUID %q for the pool %q", peer.UUID, pool)
|
|
}
|
|
log.NamedInfo(nsName, logger, "successfully removed peer site %q", peer.UUID)
|
|
}
|
|
}
|
|
|
|
// Disable mirroring on pool
|
|
err = cephclient.DisablePoolMirroring(r.context, r.clusterInfo, pool)
|
|
if err != nil {
|
|
return errors.Wrapf(err, "failed to disable mirroring for pool %q", pool)
|
|
}
|
|
log.NamedInfo(nsName, logger, "successfully disabled mirroring on the pool %q", pool)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *ReconcileCephBlockPool) isAnyRadosNamespaceMirrored(poolName string) (bool, error) {
|
|
nsName := opcontroller.NsName(r.clusterInfo.Namespace, poolName)
|
|
log.NamedDebug(nsName, logger, "list rados namespace in pool")
|
|
|
|
list, err := cephclient.ListRadosNamespacesInPool(r.context, r.clusterInfo, poolName)
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to list rados namespace in pool %q", poolName)
|
|
}
|
|
log.NamedDebug(nsName, logger, "rados namespace list %v in pool", list)
|
|
|
|
for _, namespace := range list {
|
|
poolAndRadosNamespaceName := fmt.Sprintf("%s/%s", poolName, namespace)
|
|
mirrorInfo, err := cephclient.GetPoolMirroringInfo(r.context, r.clusterInfo, poolAndRadosNamespaceName)
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to get mirroring info for the rados namespace %q", poolAndRadosNamespaceName)
|
|
}
|
|
log.NamedDebug(nsName, logger, "mirroring info for the rados namespace %q: %v", poolAndRadosNamespaceName, mirrorInfo)
|
|
if mirrorInfo.Mode != "disabled" {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func canConfigurePoolMirroring(poolSpec cephv1.NamedPoolSpec) bool {
|
|
// if the pool is erasure coded, we should not enable mirroring on it.
|
|
if poolSpec.IsErasureCoded() {
|
|
return false
|
|
}
|
|
return true
|
|
}
|