Merge pull request #14776 from sp98/osd-migration

migrate OSDs to enable encryption as day-2 operation
This commit is contained in:
Travis Nielsen authored and GitHub committed 2024-12-05 23:24:42 -07:00
commit 476827eff6
23 files changed
+756 -583

No files matched your search

@@ -357,6 +357,7 @@ The following storage selection settings are specific to Ceph and do not apply t
* `encryptedDevice`**: Encrypt OSD volumes using dmcrypt ("true" or "false"). By default this option is disabled. See [encryption](http://docs.ceph.com/docs/master/ceph-volume/lvm/encryption/) for more information on encryption in Ceph. (Resizing is not supported for host-based clusters.)
* `crushRoot`: The value of the `root` CRUSH map label. The default is `default`. Generally, you should not need to change this. However, if any of your topology labels may have the value `default`, you need to change `crushRoot` to avoid conflicts, since CRUSH map values need to be unique.
* `enableCrushUpdates`: Enables rook to update the pool crush rule using Pool Spec. Can cause data remapping if crush rule changes, Defaults to false.
* `migration`: Existing PVC based OSDs can be migrated to enable or disable encryption. Refer to the [osd management](../../Storage-Configuration/Advanced/ceph-osd-mgmt.md/#osd-encryption-as-day-2-operation) topic for details.
Allowed configurations are:
+85
View File
@@ -8049,6 +8049,65 @@ bool
</tr>
</tbody>
</table>
<h3 id="ceph.rook.io/v1.Migration">Migration
</h3>
<p>
(<em>Appears on:</em><a href="#ceph.rook.io/v1.StorageScopeSpec">StorageScopeSpec</a>)
</p>
<div>
<p>Migration handles the OSD migration</p>
</div>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>confirmation</code><br/>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>A user confirmation to migrate the OSDs. It destroys each OSD one at a time, cleans up the backing disk
and prepares OSD with same ID on that disk</p>
</td>
</tr>
</tbody>
</table>
<h3 id="ceph.rook.io/v1.MigrationStatus">MigrationStatus
</h3>
<p>
(<em>Appears on:</em><a href="#ceph.rook.io/v1.OSDStatus">OSDStatus</a>)
</p>
<div>
<p>MigrationStatus status represents the current status of any OSD migration.</p>
</div>
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>pending</code><br/>
<em>
int
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="ceph.rook.io/v1.MirrorHealthCheckSpec">MirrorHealthCheckSpec
</h3>
<p>
@@ -9471,6 +9530,18 @@ map[string]int
<p>StoreType is a mapping between the OSD backend stores and number of OSDs using these stores</p>
</td>
</tr>
<tr>
<td>
<code>migrationStatus</code><br/>
<em>
<a href="#ceph.rook.io/v1.MigrationStatus">
MigrationStatus
</a>
</em>
</td>
<td>
</td>
</tr>
</tbody>
</table>
<h3 id="ceph.rook.io/v1.OSDStore">OSDStore
@@ -12935,6 +13006,20 @@ Selection
</tr>
<tr>
<td>
<code>migration</code><br/>
<em>
<a href="#ceph.rook.io/v1.Migration">
Migration
</a>
</em>
</td>
<td>
<em>(Optional)</em>
<p>Migration handles the OSD migration</p>
</td>
</tr>
<tr>
<td>
<code>store</code><br/>
<em>
<a href="#ceph.rook.io/v1.OSDStore">
@@ -190,3 +190,29 @@ If you don't see a new OSD automatically created, restart the operator (by delet
!!! note
The OSD might have a different ID than the previous OSD that was replaced.
## OSD Migration
Ceph does not support changing certain settings on existing OSDs. To support changing these settings on an OSD, the OSD must be destroyed and re-created with the new settings. Rook will automate this by migrating only one OSD at a time. The operator waits for the data to rebalance (PGs to become `active+clean`) before migrating the next OSD. This ensures that there is no data loss. Refer to the [OSD migration](https://github.com/rook/rook/blob/master/design/ceph/osd-migration.md) design doc for more information.
The following scenarios are supported for OSD migration:
- Enable or disable OSD encryption for existing PVC-based OSDs by changing the `encrypted` setting under the `storageClassDeviceSets`
For example:
```yaml
storage:
migration:
confirmation: "yes-really-migrate-osds"
storageClassDeviceSets:
- name: set1
count: 3
encrypted: true # change to true or false based on whether encryption needs to enable or disabled.
```
Details about the migration status can be found under the cephCluster `status.storage.osd.migrationStatus.pending` field which shows the total number of OSDs that are pending migration.
!!! note
Performance of the cluster might be impacted during data rebalancing while OSDs are being migrated.
+1
View File
@@ -8,3 +8,4 @@
- Enable mirroring for CephBlockPoolRadosNamespaces (see [#14701](https://github.com/rook/rook/pull/14701)).
- Enable periodic monitoring for CephBlockPoolRadosNamespaces mirroring (see [#14896](https://github.com/rook/rook/pull/14896)).
- Allow migration of PVC based OSDs to enable or disable encryption (see [#14776](https://github.com/rook/rook/pull/14776)).
+2 -2
View File
@@ -261,10 +261,10 @@ func prepareOSD(cmd *cobra.Command, args []string) error {
}
// destroy the OSD using the OSD ID
var replaceOSD *oposd.OSDReplaceInfo
var replaceOSD *oposd.OSDInfo
if replaceOSDID != -1 {
logger.Infof("destroying osd.%d and cleaning its backing device", replaceOSDID)
replaceOSD, err = osddaemon.DestroyOSD(context, &clusterInfo, replaceOSDID, cfg.pvcBacked, cfg.storeConfig.EncryptedDevice)
replaceOSD, err = osddaemon.DestroyOSD(context, &clusterInfo, replaceOSDID, cfg.pvcBacked)
if err != nil {
rook.TerminateFatal(errors.Wrapf(err, "failed to destroy OSD %d.", replaceOSDID))
}
@@ -3445,6 +3445,16 @@ spec:
minimum: 0
nullable: true
type: number
migration:
description: Migration handles the OSD migration
properties:
confirmation:
description: |-
A user confirmation to migrate the OSDs. It destroys each OSD one at a time, cleans up the backing disk
and prepares OSD with same ID on that disk
pattern: ^$|^yes-really-migrate-osds$
type: string
type: object
nearFullRatio:
description: NearFullRatio is the ratio at which the cluster is considered nearly full and will raise a ceph health warning. Default is 0.85.
maximum: 1
@@ -5538,6 +5548,12 @@ spec:
osd:
description: OSDStatus represents OSD status of the ceph Cluster
properties:
migrationStatus:
description: MigrationStatus status represents the current status of any OSD migration.
properties:
pending:
type: integer
type: object
storeType:
additionalProperties:
type: integer
+16
View File
@@ -3443,6 +3443,16 @@ spec:
minimum: 0
nullable: true
type: number
migration:
description: Migration handles the OSD migration
properties:
confirmation:
description: |-
A user confirmation to migrate the OSDs. It destroys each OSD one at a time, cleans up the backing disk
and prepares OSD with same ID on that disk
pattern: ^$|^yes-really-migrate-osds$
type: string
type: object
nearFullRatio:
description: NearFullRatio is the ratio at which the cluster is considered nearly full and will raise a ceph health warning. Default is 0.85.
maximum: 1
@@ -5536,6 +5546,12 @@ spec:
osd:
description: OSDStatus represents OSD status of the ceph Cluster
properties:
migrationStatus:
description: MigrationStatus status represents the current status of any OSD migration.
properties:
pending:
type: integer
type: object
storeType:
additionalProperties:
type: integer
+1
View File
@@ -25,6 +25,7 @@ considered in the future:
and application data on slower media.
- Setups with multiple OSDs per drive, though with recent Ceph releases the
motivation for deploying this way is mostly obviated.
- OSDs where Persistent Volumes are using partitioned disks due to a [ceph issue](https://tracker.ceph.com/issues/68977).
## Proposal
- Since migration requires destroying of the OSD and cleaning data from the disk,
+19 -1
View File
@@ -500,7 +500,13 @@ type DeviceClasses struct {
// OSDStatus represents OSD status of the ceph Cluster
type OSDStatus struct {
// StoreType is a mapping between the OSD backend stores and number of OSDs using these stores
StoreType map[string]int `json:"storeType,omitempty"`
StoreType map[string]int `json:"storeType,omitempty"`
MigrationStatus MigrationStatus `json:"migrationStatus,omitempty"`
}
// MigrationStatus status represents the current status of any OSD migration.
type MigrationStatus struct {
Pending int `json:"pending,omitempty"`
}
// ClusterVersion represents the version of a Ceph Cluster
@@ -3051,6 +3057,9 @@ type StorageScopeSpec struct {
// +nullable
// +optional
StorageClassDeviceSets []StorageClassDeviceSet `json:"storageClassDeviceSets,omitempty"`
// Migration handles the OSD migration
// +optional
Migration Migration `json:"migration,omitempty"`
// +optional
Store OSDStore `json:"store,omitempty"`
// +optional
@@ -3089,6 +3098,15 @@ type StorageScopeSpec struct {
AllowOsdCrushWeightUpdate bool `json:"allowOsdCrushWeightUpdate,omitempty"`
}
// Migration handles the OSD migration
type Migration struct {
// A user confirmation to migrate the OSDs. It destroys each OSD one at a time, cleans up the backing disk
// and prepares OSD with same ID on that disk
// +optional
// +kubebuilder:validation:Pattern=`^$|^yes-really-migrate-osds$`
Confirmation string `json:"confirmation,omitempty"`
}
// OSDStore is the backend storage type used for creating the OSDs
type OSDStore struct {
// Type of backend storage to be used while creating OSDs. If empty, then bluestore will be used
@@ -3081,6 +3081,38 @@ func (in *MgrSpec) DeepCopy() *MgrSpec {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Migration) DeepCopyInto(out *Migration) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Migration.
func (in *Migration) DeepCopy() *Migration {
if in == nil {
return nil
}
out := new(Migration)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MigrationStatus) DeepCopyInto(out *MigrationStatus) {
*out = *in
return
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MigrationStatus.
func (in *MigrationStatus) DeepCopy() *MigrationStatus {
if in == nil {
return nil
}
out := new(MigrationStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *MirrorHealthCheckSpec) DeepCopyInto(out *MirrorHealthCheckSpec) {
*out = *in
@@ -3615,6 +3647,7 @@ func (in *OSDStatus) DeepCopyInto(out *OSDStatus) {
(*out)[key] = val
}
}
out.MigrationStatus = in.MigrationStatus
return
}
@@ -4799,6 +4832,7 @@ func (in *StorageScopeSpec) DeepCopyInto(out *StorageScopeSpec) {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
out.Migration = in.Migration
out.Store = in.Store
if in.FullRatio != nil {
in, out := &in.FullRatio, &out.FullRatio
+3 -3
View File
@@ -38,13 +38,13 @@ type OsdAgent struct {
storeConfig config.StoreConfig
kv *k8sutil.ConfigMapKVStore
pvcBacked bool
replaceOSD *oposd.OSDReplaceInfo
replaceOSD *oposd.OSDInfo
}
// NewAgent is the instantiation of the OSD agent
func NewAgent(context *clusterd.Context, devices []DesiredDevice, metadataDevice string, forceFormat bool,
storeConfig config.StoreConfig, clusterInfo *cephclient.ClusterInfo, nodeName string, kv *k8sutil.ConfigMapKVStore,
replaceOSD *oposd.OSDReplaceInfo, pvcBacked bool) *OsdAgent {
replaceOSD *oposd.OSDInfo, pvcBacked bool) *OsdAgent {
return &OsdAgent{
devices: devices,
@@ -71,7 +71,7 @@ func getDeviceLVPath(context *clusterd.Context, deviceName string) string {
// GetReplaceOSDId returns the OSD ID based on the device name
func (a *OsdAgent) GetReplaceOSDId(device string) int {
if device == a.replaceOSD.Path {
if device == a.replaceOSD.BlockPath {
return a.replaceOSD.ID
}
+8 -11
View File
@@ -248,15 +248,12 @@ func archiveCrash(clusterdContext *clusterd.Context, clusterInfo *client.Cluster
}
// DestroyOSD fetches the OSD to be replaced 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, isEncrypted bool) (*oposd.OSDReplaceInfo, error) {
var block string
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)
}
block = osdInfo.BlockPath
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()
@@ -265,7 +262,7 @@ func DestroyOSD(context *clusterd.Context, clusterInfo *client.ClusterInfo, id i
}
logger.Infof("successfully destroyed osd.%d", osdInfo.ID)
if isPVC && isEncrypted {
if isPVC && osdInfo.Encrypted {
// remove the dm device
pvcName := os.Getenv(oposd.PVCNameEnvVarName)
target := oposd.EncryptionDMName(pvcName, oposd.DmcryptBlockType)
@@ -280,17 +277,17 @@ func DestroyOSD(context *clusterd.Context, clusterInfo *client.ClusterInfo, id i
if err != nil {
return nil, errors.Wrapf(err, "failed to get device info for %q", blockPath)
}
block = diskInfo.RealPath
osdInfo.BlockPath = diskInfo.RealPath
}
logger.Infof("zap OSD.%d path %q", osdInfo.ID, block)
output, err := context.Executor.ExecuteCommandWithCombinedOutput("stdbuf", "-oL", "ceph-volume", "lvm", "zap", block, "--destroy")
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, block, output)
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, block)
logger.Infof("successfully zapped osd.%d path %q", osdInfo.ID, osdInfo.BlockPath)
return &oposd.OSDReplaceInfo{ID: osdInfo.ID, Path: block}, nil
return osdInfo, nil
}
+1 -1
View File
@@ -1180,7 +1180,7 @@ func GetCephVolumeRawOSDs(context *clusterd.Context, clusterInfo *client.Cluster
// pod
// For the cleanup pod we don't want to close the encrypted block since it will sanitize it
// first and then close it
if os.Getenv(oposd.CephVolumeEncryptedKeyEnvVarName) != "" {
if osd.Encrypted && os.Getenv(oposd.CephVolumeEncryptedKeyEnvVarName) != "" {
// If label and subsystem are not set on the encrypted block let's set it
// They will be set if the OSD deployment has been removed manually and the prepare job
// runs again.
+2 -2
View File
@@ -1574,7 +1574,7 @@ func TestInitializeBlockPVC(t *testing.T) {
assert.Equal(t, "", walBlockPath)
// Test for condition when osd is prepared with existing osd ID
a = &OsdAgent{clusterInfo: clusterInfo, nodeName: "node1", storeConfig: config.StoreConfig{StoreType: "bluestore"}, replaceOSD: &oposd.OSDReplaceInfo{ID: 3, Path: "/dev/sda"}}
a = &OsdAgent{clusterInfo: clusterInfo, nodeName: "node1", storeConfig: config.StoreConfig{StoreType: "bluestore"}, replaceOSD: &oposd.OSDInfo{ID: 3, BlockPath: "/dev/sda"}}
devices = &DeviceOsdMapping{
Entries: map[string]*DeviceOsdIDEntry{
"data": {Data: -1, Metadata: nil, Config: DesiredDevice{Name: "/mnt/set1-data-0-rpf2k"}, DeviceInfo: &sys.LocalDisk{RealPath: "/dev/sda"}},
@@ -1596,7 +1596,7 @@ func TestInitializeBlockPVC(t *testing.T) {
assert.Equal(t, "", walBlockPath)
// Test for condition that --osd-id is not passed for the devices that don't match the OSD to be replaced.
a = &OsdAgent{clusterInfo: clusterInfo, nodeName: "node1", storeConfig: config.StoreConfig{StoreType: "bluestore"}, replaceOSD: &oposd.OSDReplaceInfo{ID: 3, Path: "/dev/sda"}}
a = &OsdAgent{clusterInfo: clusterInfo, nodeName: "node1", storeConfig: config.StoreConfig{StoreType: "bluestore"}, replaceOSD: &oposd.OSDInfo{ID: 3, BlockPath: "/dev/sda"}}
devices = &DeviceOsdMapping{
Entries: map[string]*DeviceOsdIDEntry{
"data": {Data: -1, Metadata: nil, Config: DesiredDevice{Name: "/mnt/set1-data-0-rpf2k"}, DeviceInfo: &sys.LocalDisk{RealPath: "/dev/sdb"}},
+3 -3
View File
@@ -187,9 +187,9 @@ func (c *Cluster) startProvisioningOverPVCs(config *provisionConfig, errs *provi
}
// Allow updating OSD prepare pod if the OSD needs migration
if c.replaceOSD != nil {
if strings.Contains(c.replaceOSD.Path, dataSource.ClaimName) {
logger.Infof("updating OSD prepare pod to replace OSD.%d", c.replaceOSD.ID)
if c.migrateOSD != nil {
if strings.Contains(c.migrateOSD.BlockPath, dataSource.ClaimName) {
logger.Infof("updating OSD prepare pod to replace OSD.%d", c.migrateOSD.ID)
skipPreparePod = false
}
}
+6
View File
@@ -64,6 +64,12 @@ func (c *Cluster) getOSDLabels(osd OSDInfo, failureDomainValue string, portable
labels[deviceType] = osd.DeviceType
}
encryptedOSD := "false"
if osd.Encrypted {
encryptedOSD = "true"
}
labels[encrypted] = encryptedOSD
for k, v := range getOSDTopologyLocationLabels(osd.Location) {
labels[k] = v
}
+217
View File
@@ -0,0 +1,217 @@
/*
Copyright 2024 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 for the Ceph OSDs.
package osd
import (
"fmt"
"strconv"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/k8sutil"
appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// OSDMigrationConfirmation is the confirmation provided by the user in the cephCluster spec.
OSDMigrationConfirmation = "yes-really-migrate-osds"
// OSDUpdateStoreConfirmation is the confirmation provided by the user to updated OSD backend store
OSDUpdateStoreConfirmation = "yes-really-update-store"
// OSDMigrationConfigName is the configMap that stores the ID of the last migrated OSD
OSDMigrationConfigName = "osd-migration-config"
// OSDIdKey is the key used to store the OSD ID inside the `osd-migration-config` configMap
OSDIdKey = "osdID"
)
// migrationConfig represents the OSDs that need migration
type migrationConfig struct {
// osds that require migration (map key is the OSD id)
osds map[int]*OSDInfo
}
func (c *Cluster) newMigrationConfig() (*migrationConfig, error) {
mc := migrationConfig{
osds: map[int]*OSDInfo{},
}
osdDeployments, err := c.getOSDDeployments()
if err != nil {
return nil, errors.Wrapf(err, "failed to get existing OSD deployments in namespace %q", c.clusterInfo.Namespace)
}
// get OSDs that require migration due to change in encryption settings
err = mc.migrateForEncryption(c, osdDeployments)
if err != nil {
return nil, errors.Wrapf(err, "failed to get OSDs that require migration due to change in encryption setting")
}
// get OSDs that require migration due the change in OSD store type settings
err = mc.migrateForOSDStore(c, osdDeployments)
if err != nil {
return nil, errors.Wrapf(err, "failed to get OSDs that require migration due to change in OSD Store type setting")
}
return &mc, nil
}
// migrateForEncryption gets all the OSDs that require migration due to change in the cephCluster encryption setting
func (m *migrationConfig) migrateForEncryption(c *Cluster, osdDeployments *appsv1.DeploymentList) error {
deviceSetMap := map[string]cephv1.StorageClassDeviceSet{}
for i := range c.spec.Storage.StorageClassDeviceSets {
deviceSetMap[c.spec.Storage.StorageClassDeviceSets[i].Name] = c.spec.Storage.StorageClassDeviceSets[i]
}
for i := range osdDeployments.Items {
osdDeviceSetName := osdDeployments.Items[i].Labels[CephDeviceSetLabelKey]
requestedEncryptionSetting := deviceSetMap[osdDeviceSetName].Encrypted
actualEncryptedSetting := false
if osdDeployments.Items[i].Labels["encrypted"] == "true" {
actualEncryptedSetting = true
}
if requestedEncryptionSetting != actualEncryptedSetting {
osdInfo, err := c.getOSDInfo(&osdDeployments.Items[i])
if err != nil {
return errors.Wrapf(err, "failed to details about the OSD %q", osdDeployments.Items[i].Name)
}
logger.Infof("migration is required for OSD.%d due to change in encryption settings from %t to %t in storageClassDeviceSet %q", osdInfo.ID, actualEncryptedSetting, requestedEncryptionSetting, osdDeviceSetName)
if _, exists := m.osds[osdInfo.ID]; !exists {
m.osds[osdInfo.ID] = &osdInfo
}
}
}
return nil
}
// migrateForOSDStore gets all the OSDs that require migration due to change in the cephCluster OSD storeType setting
func (m *migrationConfig) migrateForOSDStore(c *Cluster, osdDeployments *appsv1.DeploymentList) error {
for i := range osdDeployments.Items {
if osdStore, ok := osdDeployments.Items[i].Labels[osdStore]; ok {
if osdStore != string(c.spec.Storage.Store.Type) {
osdInfo, err := c.getOSDInfo(&osdDeployments.Items[i])
if err != nil {
return errors.Wrapf(err, "failed to details about the OSD %q", osdDeployments.Items[i].Name)
}
logger.Infof("migration is required for OSD.%d to update storeType from %q to %q", osdInfo.ID, osdStore, c.spec.Storage.Store.Type)
if _, exists := m.osds[osdInfo.ID]; !exists {
m.osds[osdInfo.ID] = &osdInfo
}
}
}
}
return nil
}
// getOSDToMigrate returns the next OSD to migrate from the list of OSDs that are pending migration.
func (m *migrationConfig) getOSDToMigrate() *OSDInfo {
osdInfo := &OSDInfo{}
osdID := -1
for k, v := range m.osds {
osdID, osdInfo = k, v
break
}
delete(m.osds, osdID)
return osdInfo
}
func (m *migrationConfig) getOSDIds() []int {
osdIds := make([]int, len(m.osds))
i := 0
for k := range m.osds {
osdIds[i] = k
i++
}
return osdIds
}
// saveMigrationConfig saves the ID of the migrated OSD to a configMap
func saveMigrationConfig(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo, osdID int) error {
newConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: OSDMigrationConfigName,
Namespace: clusterInfo.Namespace,
},
Data: map[string]string{
OSDIdKey: strconv.Itoa(osdID),
},
}
err := clusterInfo.OwnerInfo.SetControllerReference(newConfigMap)
if err != nil {
return errors.Wrapf(err, "failed to set owner reference on %q configMap", newConfigMap.Name)
}
_, err = k8sutil.CreateOrUpdateConfigMap(clusterInfo.Context, context.Clientset, newConfigMap)
if err != nil {
return errors.Wrapf(err, "failed to create or update %q configMap", newConfigMap.Name)
}
return nil
}
// isLastOSDMigrationComplete checks if the deployment for the migrated OSD got created successfully.
func isLastOSDMigrationComplete(c *Cluster) (bool, error) {
migratedOSDId, err := getLastMigratedOSDId(c.context, c.clusterInfo)
if err != nil {
return false, errors.Wrapf(err, "failed to get last migrated OSD ID")
}
if migratedOSDId == -1 {
return true, nil
}
deploymentName := fmt.Sprintf("rook-ceph-osd-%d", migratedOSDId)
_, err = c.context.Clientset.AppsV1().Deployments(c.clusterInfo.Namespace).Get(c.clusterInfo.Context, deploymentName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
logger.Infof("deployment for the last migrated OSD with ID - %d is not found.", migratedOSDId)
return false, nil
}
}
logger.Infof("migration of OSD.%d was successful", migratedOSDId)
return true, nil
}
// getLastMigratedOSDId fetches the ID of the last migrated OSD from the "osd-migration-config" configmap
func getLastMigratedOSDId(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo) (int, error) {
cm, err := context.Clientset.CoreV1().ConfigMaps(clusterInfo.Namespace).Get(clusterInfo.Context, OSDMigrationConfigName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
return -1, nil
}
}
osdID, ok := cm.Data[OSDIdKey]
if !ok || osdID == "" {
logger.Debugf("empty config map %q", OSDMigrationConfigName)
return -1, nil
}
osdIDInt, err := strconv.Atoi(osdID)
if err != nil {
return -1, errors.Wrapf(err, "failed to convert OSD id %q to integer.", osdID)
}
return osdIDInt, nil
}
@@ -0,0 +1,225 @@
/*
Copyright 2024 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 for the Ceph OSDs.
package osd
import (
"context"
"testing"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestMigrateForEncryption(t *testing.T) {
namespace := "rook-ceph"
namespace2 := "rook-ceph2"
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
Context: context.TODO(),
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
c := New(ctx, clusterInfo, cephv1.ClusterSpec{}, "rook/rook:master")
t.Run("no OSD migration is required due to encryption", func(t *testing.T) {
c.spec.Storage.StorageClassDeviceSets = []cephv1.StorageClassDeviceSet{
{
Name: "set1",
Encrypted: true,
},
{
Name: "set2",
Encrypted: true,
},
}
d1 := getDummyDeploymentOnNode(clientset, c, "node2", 1)
d1.Labels["encrypted"] = "true"
d1.Labels["ceph.rook.io/DeviceSet"] = "set1"
createDeploymentOrPanic(clientset, d1)
d2 := getDummyDeploymentOnNode(clientset, c, "node2", 2)
d2.Labels["encrypted"] = "true"
d2.Labels["ceph.rook.io/DeviceSet"] = "set1"
createDeploymentOrPanic(clientset, d2)
deployments, err := c.getOSDDeployments()
assert.NoError(t, err)
mc := migrationConfig{
osds: map[int]*OSDInfo{},
}
err = mc.migrateForEncryption(c, deployments)
assert.NoError(t, err)
assert.Equal(t, 0, len(mc.osds))
})
t.Run("osd.1 on set1 needs migration", func(t *testing.T) {
c.clusterInfo.Namespace = namespace2
c.spec.Storage.StorageClassDeviceSets = []cephv1.StorageClassDeviceSet{
{
Name: "set1",
Encrypted: true,
},
}
d1 := getDummyDeploymentOnNode(clientset, c, "node2", 1)
d1.Labels["encrypted"] = "false"
d1.Labels["ceph.rook.io/DeviceSet"] = "set1"
createDeploymentOrPanic(clientset, d1)
deployments, err := c.getOSDDeployments()
assert.NoError(t, err)
mc := migrationConfig{
osds: map[int]*OSDInfo{},
}
err = mc.migrateForEncryption(c, deployments)
assert.NoError(t, err)
assert.Equal(t, 1, len(mc.osds))
assert.Equal(t, 1, mc.osds[1].ID)
})
}
func TestMigrationForOSDStore(t *testing.T) {
namespace := "rook-ceph"
namespace2 := "rook-ceph2"
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
Context: context.TODO(),
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
c := New(ctx, clusterInfo, cephv1.ClusterSpec{}, "rook/rook:master")
t.Run("no OSD migration is required due to OSD store change", func(t *testing.T) {
c.spec.Storage.Store.Type = "store1"
d1 := getDummyDeploymentOnNode(clientset, c, "node2", 1)
d1.Labels[osdStore] = "store1"
createDeploymentOrPanic(clientset, d1)
d2 := getDummyDeploymentOnNode(clientset, c, "node2", 2)
d2.Labels[osdStore] = "store1"
createDeploymentOrPanic(clientset, d2)
deployments, err := c.getOSDDeployments()
assert.NoError(t, err)
mc := migrationConfig{
osds: map[int]*OSDInfo{},
}
err = mc.migrateForEncryption(c, deployments)
assert.NoError(t, err)
assert.Equal(t, 0, len(mc.osds))
})
t.Run("osd.1 needs migration due to change is OSD store type", func(t *testing.T) {
c.clusterInfo.Namespace = namespace2
c.spec.Storage.Store.Type = "newStore"
d1 := getDummyDeploymentOnNode(clientset, c, "node2", 1)
d1.Labels[osdStore] = "store1" // store type is set to `store1` but spec has `newStore`
createDeploymentOrPanic(clientset, d1)
d2 := getDummyDeploymentOnNode(clientset, c, "node2", 2)
d2.Labels[osdStore] = "newStore" // store type label matches with the spec
createDeploymentOrPanic(clientset, d2)
deployments, err := c.getOSDDeployments()
assert.NoError(t, err)
mc := migrationConfig{
osds: map[int]*OSDInfo{},
}
err = mc.migrateForOSDStore(c, deployments)
assert.NoError(t, err)
assert.Equal(t, 1, len(mc.osds))
assert.Equal(t, 1, mc.osds[1].ID)
})
}
func createMigrationConfigmap(osdID, ns string, clientset *fake.Clientset) error {
ctx := context.TODO()
data := make(map[string]string, 1)
data[OSDIdKey] = osdID
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: OSDMigrationConfigName,
Namespace: ns,
},
Data: data,
}
_, err := clientset.CoreV1().ConfigMaps(ns).Create(ctx, cm, metav1.CreateOptions{})
return err
}
func TestIsLastOSDMigrationComplete(t *testing.T) {
namespace := "rook-ceph"
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
Context: context.TODO(),
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
c := New(ctx, clusterInfo, cephv1.ClusterSpec{}, "rook/rook:master")
t.Run("no OSD migration config found", func(t *testing.T) {
result, err := isLastOSDMigrationComplete(c)
assert.NoError(t, err)
assert.Equal(t, true, result)
})
t.Run("osd.1 was migrated but its not up yet", func(t *testing.T) {
err := createMigrationConfigmap("1", namespace, clientset)
assert.NoError(t, err)
result, err := isLastOSDMigrationComplete(c)
assert.NoError(t, err)
assert.Equal(t, false, result)
})
t.Run("migrated osd.1 is up", func(t *testing.T) {
d1 := getDummyDeploymentOnNode(clientset, c, "node2", 1)
createDeploymentOrPanic(clientset, d1)
result, err := isLastOSDMigrationComplete(c)
assert.NoError(t, err)
assert.Equal(t, true, result)
})
}
+85 -33
View File
@@ -74,6 +74,7 @@ const (
deviceClass = "device-class"
osdStore = "osd-store"
deviceType = "device-type"
encrypted = "encrypted"
)
// Cluster keeps track of the OSDs
@@ -85,7 +86,7 @@ type Cluster struct {
ValidStorage cephv1.StorageScopeSpec // valid subset of `Storage`, computed at runtime
kv *k8sutil.ConfigMapKVStore
deviceSets []deviceSet
replaceOSD *OSDReplaceInfo
migrateOSD *OSDInfo
deprecatedOSDs map[string][]int
}
@@ -218,26 +219,27 @@ func (c *Cluster) Start() error {
}
logger.Infof("wait timeout for healthy OSDs during upgrade or restart is %q", c.clusterInfo.OsdUpgradeTimeout)
// replace OSDs for a new backing store
osdsToBeReplaced, err := c.replaceOSDForNewStore()
if err != nil {
return errors.Wrapf(err, "failed to replace OSD for new backing store %q in namespace %q", c.spec.Storage.Store.Type, namespace)
}
osdsToSkipReconcile, err := controller.GetDaemonsToSkipReconcile(c.clusterInfo.Context, c.context, c.clusterInfo.Namespace, OsdIdLabelKey, AppName)
if err != nil {
logger.Warningf("failed to get osds to skip reconcile. %v", err)
}
migrationConfig, err := c.startOSDMigration()
if err != nil {
return errors.Wrapf(err, "failed to start OSD migration")
}
// prepare for updating existing OSDs
updateQueue, deployments, err := c.getOSDUpdateInfo(errs)
if err != nil {
return errors.Wrapf(err, "failed to get information about currently-running OSD Deployments in namespace %q", namespace)
}
// OSDs that are to be replaced should not be upgraded. So remove them from the `updateQueue`
if len(osdsToBeReplaced) > 0 {
updateQueue.Remove(osdsToBeReplaced.getOSDIds())
if migrationConfig != nil {
if len(migrationConfig.osds) != 0 {
// prevent upgrade of OSDs that require migration
updateQueue.Remove(migrationConfig.getOSDIds())
}
}
logger.Debugf("%d of %d OSD Deployments need update", updateQueue.Len(), deployments.Len())
@@ -295,32 +297,69 @@ func (c *Cluster) Start() error {
return errors.Wrapf(err, "failed to update ceph storage status")
}
if c.spec.Storage.Store.UpdateStore == OSDStoreUpdateConfirmation {
delOpts := &k8sutil.DeleteOptions{WaitOptions: k8sutil.WaitOptions{Wait: true}}
err := k8sutil.DeleteConfigMap(c.clusterInfo.Context, c.context.Clientset, OSDReplaceConfigName, namespace, delOpts)
if err != nil {
if kerrors.IsNotFound(err) {
logger.Debugf("config map %q not found. Ignoring since object must be deleted.", OSDReplaceConfigName)
} else {
return errors.Wrapf(err, "failed to delete the %q configmap", OSDReplaceConfigName)
}
}
if len(osdsToBeReplaced) > 0 {
// wait for the pgs to be healthy before attempting to migrate the next OSD
_, err := c.waitForHealthyPGs()
if err != nil {
return errors.Wrapf(err, "failed to wait for pgs to be healhty")
}
// return with error to reconcile the operator since there are OSDs that are pending migration
return errors.New("reconcile operator to replace OSDs that are pending migration")
}
}
logger.Infof("finished running OSDs in namespace %q", namespace)
return nil
}
func (c *Cluster) startOSDMigration() (*migrationConfig, error) {
if !c.isMigrationRequested() {
logger.Debug("no OSD migration is requested")
return nil, nil
}
logger.Info("osd migration is requested")
// start migration only if PGs are active+clean
pgsHealhty, err := c.waitForHealthyPGs()
if err != nil {
return nil, errors.Wrapf(err, "failed to wait for pgs to be healthy")
}
if !pgsHealhty {
return nil, errors.Wrapf(err, "failed to start migration due to unhealthy PGs")
}
// skip migration if previously migrated OSD is not up yet.
migrationComplete, err := isLastOSDMigrationComplete(c)
if err != nil {
return nil, errors.Wrapf(err, "failed to check if the last migration was successful or not")
}
if !migrationComplete {
return nil, errors.Wrapf(err, "migration of the last OSD is not complete")
}
migrationConfig, err := c.newMigrationConfig()
if err != nil {
return nil, errors.Wrapf(err, "failed to get new OSD migration config")
}
// delete deployment of the osd that needs migration
if migrationConfig != nil && len(migrationConfig.osds) > 0 {
osdToMigrate := migrationConfig.getOSDToMigrate()
logger.Infof("deleting OSD.%d deployment for migration ", osdToMigrate.ID)
err = c.deleteOSDDeployment(osdToMigrate.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to delete deployment for osd.%d that needs migration %q", osdToMigrate.ID, c.clusterInfo.Namespace)
}
err = saveMigrationConfig(c.context, c.clusterInfo, osdToMigrate.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to save migrated OSD ID %din the config map", osdToMigrate.ID)
}
c.migrateOSD = osdToMigrate
}
return migrationConfig, nil
}
func (c *Cluster) isMigrationRequested() bool {
// check for OSDUpdateStoreConfirmation as well for backwards compatibility
if c.spec.Storage.Migration.Confirmation == OSDMigrationConfirmation || c.spec.Storage.Store.UpdateStore == OSDUpdateStoreConfirmation {
return true
}
return false
}
func (c *Cluster) postReconcileUpdateOSDProperties(desiredOSDs map[int]*OSDInfo) error {
osdUsage, err := cephclient.GetOSDUsage(c.context, c.clusterInfo)
if err != nil {
@@ -671,6 +710,10 @@ func (c *Cluster) getOSDInfo(d *appsv1.Deployment) (OSDInfo, error) {
}
osd.Store = d.Labels[osdStore]
osd.Encrypted = false
if d.Labels[encrypted] == "true" {
osd.Encrypted = true
}
if isPVC {
osd.PVCName = d.Labels[OSDOverPVCLabelKey]
@@ -894,7 +937,7 @@ func (c *Cluster) deleteOSDDeployment(osdID int) error {
}
}
return c.replaceOSD.saveAsConfig(c.context, c.clusterInfo)
return nil
}
func (c *Cluster) waitForHealthyPGs() (bool, error) {
@@ -941,6 +984,15 @@ func (c *Cluster) updateCephStorageStatus() error {
// Add the status about deprecated OSDs
cephClusterStorage.DeprecatedOSDs = c.deprecatedOSDs
// Update pending migration status
if c.isMigrationRequested() {
migrationConfig, err := c.newMigrationConfig()
if err != nil {
return errors.Wrapf(err, "failed to get osd migration config to update cluster status")
}
cephClusterStorage.OSD.MigrationStatus.Pending = len(migrationConfig.osds)
}
err = c.context.Client.Get(c.clusterInfo.Context, c.clusterInfo.NamespacedName(), &cephCluster)
if err != nil {
if kerrors.IsNotFound(err) {
-169
View File
@@ -18,7 +18,6 @@ package osd
import (
"context"
"encoding/json"
"testing"
"github.com/pkg/errors"
@@ -796,174 +795,6 @@ func TestGetOSDInfoWithCustomRoot(t *testing.T) {
assert.Error(t, err)
}
func TestReplaceOSDForNewStore(t *testing.T) {
clusterInfo := &cephclient.ClusterInfo{Namespace: "ns", Context: context.TODO()}
clusterInfo.SetName("test")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if args[0] == "status" {
return healthyCephStatus, nil
}
return "", errors.Errorf("unexpected ceph command '%v'", args)
},
}
clientset := fake.NewSimpleClientset()
context := &clusterd.Context{
Clientset: clientset,
Executor: executor,
}
t.Run("no osd migration is requested in the cephcluster spec", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{},
},
}
c := New(context, clusterInfo, spec, "myversion")
_, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.Nil(t, c.replaceOSD)
})
t.Run("migration is requested but no osd pods are running", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
UpdateStore: "yes-really-update-store",
},
},
}
c := New(context, clusterInfo, spec, "myversion")
_, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.Nil(t, c.replaceOSD)
})
t.Run("migration is requested but all OSDs are running on expected backed store", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
UpdateStore: "yes-really-update-store",
},
},
}
c := New(context, clusterInfo, spec, "myversion")
d := getDummyDeploymentOnNode(clientset, c, "node2", 0)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
osdsToBeReplaced, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.Equal(t, 0, len(osdsToBeReplaced))
assert.Nil(t, c.replaceOSD)
})
t.Run("migration is requested and one OSD on node is running legacy backend store", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
UpdateStore: "yes-really-update-store",
},
},
}
c := New(context, clusterInfo, spec, "myversion")
// create osd deployment with `bluestore` backend store
d := getDummyDeploymentOnNode(clientset, c, "node2", 1)
createDeploymentOrPanic(clientset, d)
_, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.NotNil(t, c.replaceOSD)
assert.Equal(t, 1, c.replaceOSD.ID)
assert.Equal(t, "node2", c.replaceOSD.Node)
// assert that OSD.1 deployment got deleted
_, err = clientset.AppsV1().Deployments(clusterInfo.Namespace).Get(clusterInfo.Context, deploymentName(1), metav1.GetOptions{})
assert.Equal(t, true, k8serrors.IsNotFound(err))
// validate the osd replace config map
actualCM, err := clientset.CoreV1().ConfigMaps(clusterInfo.Namespace).Get(clusterInfo.Context, OSDReplaceConfigName, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotNil(t, actualCM)
expectedOSDInfo := OSDReplaceInfo{}
err = json.Unmarshal([]byte(actualCM.Data[OSDReplaceConfigKey]), &expectedOSDInfo)
assert.NoError(t, err)
assert.Equal(t, 1, expectedOSDInfo.ID)
assert.Equal(t, "node2", expectedOSDInfo.Node)
// delete configmap
err = k8sutil.DeleteConfigMap(clusterInfo.Context, clientset, OSDReplaceConfigName, clusterInfo.Namespace, &k8sutil.DeleteOptions{})
assert.NoError(t, err)
})
t.Run("migration is requested and one osd on pvc is running on legacy backend store", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
UpdateStore: "yes-really-update-store",
},
},
}
c := New(context, clusterInfo, spec, "myversion")
d := getDummyDeploymentOnPVC(clientset, c, "pvc1", 2)
createDeploymentOrPanic(clientset, d)
osdsToBeReplaced, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.Equal(t, 1, len(osdsToBeReplaced))
assert.NotNil(t, c.replaceOSD)
assert.Equal(t, 2, c.replaceOSD.ID)
assert.Equal(t, "pvc1", c.replaceOSD.Path)
// assert that OSD.2 deployment got deleted
_, err = clientset.AppsV1().Deployments(clusterInfo.Namespace).Get(clusterInfo.Context, deploymentName(2), metav1.GetOptions{})
assert.Equal(t, true, k8serrors.IsNotFound(err))
// validate the osd replace config map
actualCM, err := clientset.CoreV1().ConfigMaps(clusterInfo.Namespace).Get(clusterInfo.Context, OSDReplaceConfigName, metav1.GetOptions{})
assert.NoError(t, err)
assert.NotNil(t, actualCM)
expectedOSDInfo := OSDReplaceInfo{}
err = json.Unmarshal([]byte(actualCM.Data[OSDReplaceConfigKey]), &expectedOSDInfo)
assert.NoError(t, err)
assert.Equal(t, 2, expectedOSDInfo.ID)
assert.Equal(t, "pvc1", c.replaceOSD.Path)
// delete configmap
err = k8sutil.DeleteConfigMap(clusterInfo.Context, clientset, OSDReplaceConfigName, clusterInfo.Namespace, &k8sutil.DeleteOptions{})
assert.NoError(t, err)
})
t.Run("migration is requested but pgs are not clean", func(t *testing.T) {
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
UpdateStore: "yes-really-update-store",
},
},
}
executor := &exectest.MockExecutor{
MockExecuteCommandWithOutput: func(command string, args ...string) (string, error) {
logger.Infof("Command: %s %v", command, args)
if args[0] == "status" {
return unHealthyCephStatus, nil
}
return "", errors.Errorf("unexpected ceph command '%v'", args)
},
}
context.Executor = executor
c := New(context, clusterInfo, spec, "myversion")
osdsToBeReplaced, err := c.replaceOSDForNewStore()
assert.NoError(t, err)
assert.Equal(t, 0, len(osdsToBeReplaced))
assert.Nil(t, c.replaceOSD)
})
}
func TestUpdateCephStorageStatus(t *testing.T) {
ctx := context.TODO()
clusterInfo := cephclient.AdminTestClusterInfo("fake")
@@ -308,16 +308,16 @@ func (c *Cluster) provisionOSDContainer(osdProps osdProperties, copyBinariesMoun
// Add OSD ID as environment variables.
// When this env is set, prepare pod job will destroy this OSD.
if c.replaceOSD != nil {
if c.migrateOSD != nil {
// Compare pvc claim name in case of OSDs on PVC
if osdProps.onPVC() {
if strings.Contains(c.replaceOSD.Path, osdProps.pvc.ClaimName) {
envVars = append(envVars, replaceOSDIDEnvVar(fmt.Sprint(c.replaceOSD.ID)))
if strings.Contains(c.migrateOSD.PVCName, osdProps.pvc.ClaimName) {
envVars = append(envVars, replaceOSDIDEnvVar(fmt.Sprint(c.migrateOSD.ID)))
}
} else {
// Compare the node name in case of OSDs on disk
if c.replaceOSD.Node == osdProps.crushHostname {
envVars = append(envVars, replaceOSDIDEnvVar(fmt.Sprint(c.replaceOSD.ID)))
if c.migrateOSD.NodeName == osdProps.crushHostname {
envVars = append(envVars, replaceOSDIDEnvVar(fmt.Sprint(c.migrateOSD.ID)))
}
}
}
-191
View File
@@ -1,191 +0,0 @@
/*
Copyright 2023 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 for the Ceph OSDs.
package osd
import (
"encoding/json"
"github.com/pkg/errors"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/rook/rook/pkg/operator/k8sutil"
v1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
OSDReplaceConfigName = "osd-replace-config"
OSDReplaceConfigKey = "config"
OSDStoreUpdateConfirmation = "yes-really-update-store"
)
// OSDReplaceInfo represents an OSD that needs to replaced
type OSDReplaceInfo struct {
ID int `json:"id"`
Path string `json:"path"`
Node string `json:"node"`
}
type OSDReplaceInfoList []OSDReplaceInfo
func (o *OSDReplaceInfoList) getOSDIds() []int {
osdIDs := []int{}
for _, osd := range *o {
osdIDs = append(osdIDs, osd.ID)
}
return osdIDs
}
func (o *OSDReplaceInfo) saveAsConfig(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo) error {
configStr, err := o.string()
if err != nil {
return errors.Wrapf(err, "failed to convert osd replace config to string")
}
newConfigMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: OSDReplaceConfigName,
Namespace: clusterInfo.Namespace,
},
Data: map[string]string{
OSDReplaceConfigKey: configStr,
},
}
err = clusterInfo.OwnerInfo.SetControllerReference(newConfigMap)
if err != nil {
return errors.Wrapf(err, "failed to set owner reference on %q configMap", newConfigMap.Name)
}
_, err = k8sutil.CreateOrUpdateConfigMap(clusterInfo.Context, context.Clientset, newConfigMap)
if err != nil {
return errors.Wrapf(err, "failed to create or update %q configMap", newConfigMap.Name)
}
return nil
}
func (o *OSDReplaceInfo) string() (string, error) {
configInBytes, err := json.Marshal(o)
if err != nil {
return "", errors.Wrap(err, "failed to marshal osd replace config")
}
return string(configInBytes), nil
}
func (c *Cluster) replaceOSDForNewStore() (OSDReplaceInfoList, error) {
if c.spec.Storage.Store.UpdateStore != OSDStoreUpdateConfirmation {
logger.Info("no replacement of osds is requested")
return nil, nil
}
osdsToBeReplaced, err := c.getOSDWithNonMatchingStore()
if err != nil {
return nil, errors.Wrapf(err, "failed to get information about the OSDs where backing store does not match the spec in namespace %q", c.clusterInfo.Namespace)
}
if len(osdsToBeReplaced) == 0 {
logger.Debug("all OSDs are using the desired backing store. No replacement is required.")
return osdsToBeReplaced, nil
}
// replace an OSD only if Pgs are healthy
pgHealthMsg, pgClean, err := cephclient.IsClusterClean(c.context, c.clusterInfo, c.spec.DisruptionManagement.PGHealthyRegex)
if err != nil {
return nil, errors.Wrapf(err, "failed to check if the pgs are clean before replacing OSDs")
}
if !pgClean {
logger.Warningf("skipping OSD replacement because pgs are not healthy. PG status: %q", pgHealthMsg)
return osdsToBeReplaced, nil
}
// Check for an existing OSDs in the configmap
osdToBeReplaced, err := GetOSDReplaceConfigMap(c.context, c.clusterInfo)
if err != nil {
return nil, errors.Wrap(err, "failed to get any existing OSD in replace configmap")
}
if osdToBeReplaced != nil {
c.replaceOSD = osdToBeReplaced
} else {
c.replaceOSD = &osdsToBeReplaced[0]
}
logger.Infof("replacing OSD.%d to the new backing store %q", c.replaceOSD.ID, c.spec.Storage.Store.Type)
err = c.deleteOSDDeployment(c.replaceOSD.ID)
if err != nil {
return nil, errors.Wrapf(err, "failed to delete OSD deployment that needs migration to new backend store in namespace %q", c.clusterInfo.Namespace)
}
return osdsToBeReplaced, nil
}
// getOSDWithNonMatchingStore returns OSDs with osd-store label different from expected store in cephCluster spec
func (c *Cluster) getOSDWithNonMatchingStore() (OSDReplaceInfoList, error) {
osdReplaceList := []OSDReplaceInfo{}
// get existing OSD deployments
osdDeployments, err := c.getOSDDeployments()
if err != nil {
return osdReplaceList, errors.Wrapf(err, "failed to get existing OSD deployments in namespace %q", c.clusterInfo.Namespace)
}
for i := range osdDeployments.Items {
if osdStore, ok := osdDeployments.Items[i].Labels[osdStore]; ok {
if osdStore != string(c.spec.Storage.Store.Type) {
osdInfo, err := c.getOSDInfo(&osdDeployments.Items[i])
if err != nil {
return nil, errors.Wrapf(err, "failed to details about the OSD %q", osdDeployments.Items[i].Name)
}
var path string
if osdInfo.PVCName != "" {
path = osdInfo.PVCName
} else {
path = osdInfo.BlockPath
}
osdReplaceList = append(osdReplaceList, OSDReplaceInfo{ID: osdInfo.ID, Path: path, Node: osdInfo.NodeName})
}
}
}
return osdReplaceList, nil
}
// GetOSDReplaceConfigMap returns the OSD replace config map
func GetOSDReplaceConfigMap(context *clusterd.Context, clusterInfo *cephclient.ClusterInfo) (*OSDReplaceInfo, error) {
cm, err := context.Clientset.CoreV1().ConfigMaps(clusterInfo.Namespace).Get(clusterInfo.Context, OSDReplaceConfigName, metav1.GetOptions{})
if err != nil {
if kerrors.IsNotFound(err) {
return nil, nil
}
}
configStr, ok := cm.Data[OSDReplaceConfigKey]
if !ok || configStr == "" {
logger.Debugf("empty config map %q", OSDReplaceConfigName)
return nil, nil
}
config := &OSDReplaceInfo{}
err = json.Unmarshal([]byte(configStr), config)
if err != nil {
return nil, errors.Wrapf(err, "failed to JSON unmarshal osd replace status from the (%q)", configStr)
}
return config, nil
}
@@ -1,162 +0,0 @@
/*
Copyright 2023 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 for the Ceph OSDs.
package osd
import (
"context"
"testing"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
"github.com/rook/rook/pkg/clusterd"
cephclient "github.com/rook/rook/pkg/daemon/ceph/client"
"github.com/stretchr/testify/assert"
appsv1 "k8s.io/api/apps/v1"
"k8s.io/client-go/kubernetes/fake"
)
func TestGetOSDWithNonMatchingStoreOnNodes(t *testing.T) {
namespace := "rook-ceph"
namespace2 := "rook-ceph2"
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
Context: context.TODO(),
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
},
},
}
c := New(ctx, clusterInfo, spec, "rook/rook:master")
var d *appsv1.Deployment
t.Run("all osd deployments are running on bluestore-rdr osd store", func(t *testing.T) {
d = getDummyDeploymentOnNode(clientset, c, "node2", 0)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnNode(clientset, c, "node3", 1)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnNode(clientset, c, "node4", 2)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
osdList, err := c.getOSDWithNonMatchingStore()
assert.NoError(t, err)
assert.Equal(t, 0, len(osdList))
})
t.Run("all osd deployments are not running on bluestore-rdr store", func(t *testing.T) {
c.clusterInfo.Namespace = namespace2
// osd.0 is still using bluestore
d = getDummyDeploymentOnNode(clientset, c, "node2", 0)
createDeploymentOrPanic(clientset, d)
// osd.1 and osd.2 are using `bluestore-rdr`
d = getDummyDeploymentOnNode(clientset, c, "node3", 1)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnNode(clientset, c, "node4", 2)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
osdList, err := c.getOSDWithNonMatchingStore()
assert.NoError(t, err)
assert.Equal(t, 1, len(osdList))
assert.Equal(t, 0, osdList[0].ID)
assert.Equal(t, "node2", osdList[0].Node)
assert.Equal(t, "/dev/vda", osdList[0].Path)
})
}
func TestGetOSDWithNonMatchingStoreOnPVCs(t *testing.T) {
namespace := "rook-ceph"
namespace2 := "rook-ceph2"
clientset := fake.NewSimpleClientset()
ctx := &clusterd.Context{
Clientset: clientset,
}
clusterInfo := &cephclient.ClusterInfo{
Namespace: namespace,
Context: context.TODO(),
}
clusterInfo.SetName("mycluster")
clusterInfo.OwnerInfo = cephclient.NewMinimumOwnerInfo(t)
spec := cephv1.ClusterSpec{
Storage: cephv1.StorageScopeSpec{
Store: cephv1.OSDStore{
Type: "bluestore-rdr",
},
},
}
c := New(ctx, clusterInfo, spec, "rook/rook:master")
var d *appsv1.Deployment
t.Run("all osd deployments are running on bluestore-rdr osd store", func(t *testing.T) {
d = getDummyDeploymentOnPVC(clientset, c, "pvc0", 0)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnPVC(clientset, c, "pvc1", 1)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnPVC(clientset, c, "pvc2", 2)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
osdList, err := c.getOSDWithNonMatchingStore()
assert.NoError(t, err)
assert.Equal(t, 0, len(osdList))
})
t.Run("all osd deployments are not running on bluestore-rdr store", func(t *testing.T) {
c.clusterInfo.Namespace = namespace2
// osd.0 is still using `bluestore`
d = getDummyDeploymentOnPVC(clientset, c, "pvc0", 0)
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnPVC(clientset, c, "pvc1", 1)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
d = getDummyDeploymentOnPVC(clientset, c, "pvc2", 2)
d.Labels[osdStore] = "bluestore-rdr"
createDeploymentOrPanic(clientset, d)
osdList, err := c.getOSDWithNonMatchingStore()
assert.NoError(t, err)
assert.Equal(t, 1, len(osdList))
assert.Equal(t, 0, osdList[0].ID)
assert.Equal(t, "pvc0", osdList[0].Path)
})
}