Files
my-rook-config/pkg/operator/ceph/object/shared_pools.go
T
2024-11-05 10:47:10 +01:00

551 lines
19 KiB
Go

package object
import (
"encoding/json"
"fmt"
"os"
"path"
"sort"
"github.com/pkg/errors"
cephv1 "github.com/rook/rook/pkg/apis/ceph.rook.io/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
)
const (
defaultPlacementCephConfigName = "default-placement"
defaultPlacementStorageClass = "STANDARD"
)
func IsNeedToCreateObjectStorePools(sharedPools cephv1.ObjectSharedPoolsSpec) bool {
for _, pp := range sharedPools.PoolPlacements {
if pp.Default {
// No need to create pools. External pools from default placement will be used
return false
}
}
if sharedPools.MetadataPoolName != "" && sharedPools.DataPoolName != "" {
// No need to create pools. Shared pools will be used
return false
}
return true
}
func validatePoolPlacements(placements []cephv1.PoolPlacementSpec) error {
hasDefault := false
names := make(map[string]struct{}, len(placements))
for _, p := range placements {
if hasDefault && p.Default {
return fmt.Errorf("invalidObjStorePoolConfig: only one placement can be set as default")
}
hasDefault = hasDefault || p.Default
if _, ok := names[p.Name]; ok {
return fmt.Errorf("invalidObjStorePoolConfig: invalid placement %s: placement names must be unique", p.Name)
}
if p.Name == defaultPlacementCephConfigName && !p.Default {
return fmt.Errorf("invalidObjStorePoolConfig: placement with name %s must be marked as default", defaultPlacementCephConfigName)
}
names[p.Name] = struct{}{}
if err := validatePoolPlacementStorageClasses(p.StorageClasses); err != nil {
return fmt.Errorf("invalidObjStorePoolConfig: invalid placement %s: %w", p.Name, err)
}
}
return nil
}
func validatePoolPlacementStorageClasses(scList []cephv1.PlacementStorageClassSpec) error {
names := make(map[string]struct{}, len(scList))
for _, sc := range scList {
if sc.Name == defaultPlacementStorageClass {
return fmt.Errorf("invalid placement StorageClass %q: %q name is reserved", sc.Name, defaultPlacementStorageClass)
}
if _, ok := names[sc.Name]; ok {
return fmt.Errorf("invalid placement StorageClass %q: name must be unique", sc.Name)
}
names[sc.Name] = struct{}{}
}
return nil
}
func adjustZonePlacementPools(zone map[string]interface{}, spec cephv1.ObjectSharedPoolsSpec) (map[string]interface{}, error) {
name, err := getObjProperty[string](zone, "name")
if err != nil {
return nil, fmt.Errorf("unable to get zone name: %w", err)
}
//deep copy source zone
zone, err = deepCopyJson(zone)
if err != nil {
return nil, fmt.Errorf("unable to deep copy config for zone %s: %w", name, err)
}
placements, err := getObjProperty[[]interface{}](zone, "placement_pools")
if err != nil {
return nil, fmt.Errorf("unable to get pool placements for zone %s: %w", name, err)
}
fromSpec := toZonePlacementPools(spec, name)
inConfig := map[string]struct{}{}
idxToRemove := map[int]struct{}{}
for i, p := range placements {
pObj, ok := p.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unable to cast pool placement to object for zone %s: %+v", name, p)
}
placementID, err := getObjProperty[string](pObj, "key")
if err != nil {
return nil, fmt.Errorf("unable to get pool placement name for zone %s: %w", name, err)
}
// check if placement should be removed
if _, inSpec := fromSpec[placementID]; !inSpec {
if placementID == defaultPlacementCephConfigName {
// 'default-placement' should always be kept as a workaround for https://tracker.ceph.com/issues/68775.
// if user specified other placement as default, then copy pool names to 'default-placement' from it:
if userDefault, inSpec := fromSpec[getDefaultPlacementName(spec)]; inSpec {
// duplicate user defined default placement under 'default-placement' name in spec to update pools on the next step
fromSpec[defaultPlacementCephConfigName] = userDefault
}
} else {
// remove placement if it is not in spec
idxToRemove[i] = struct{}{}
continue
}
}
// update placement with values from spec:
if pSpec, inSpec := fromSpec[placementID]; inSpec {
_, err = updateObjProperty(pObj, pSpec.Val.IndexPool, "val", "index_pool")
if err != nil {
return nil, fmt.Errorf("unable to set index pool to pool placement %q for zone %q: %w", placementID, name, err)
}
_, err = updateObjProperty(pObj, pSpec.Val.DataExtraPool, "val", "data_extra_pool")
if err != nil {
return nil, fmt.Errorf("unable to set data extra pool to pool placement %q for zone %q: %w", placementID, name, err)
}
scObj, err := toObj(pSpec.Val.StorageClasses)
if err != nil {
return nil, fmt.Errorf("unable convert to pool placement %q storage class for zone %q: %w", placementID, name, err)
}
_, err = updateObjProperty(pObj, scObj, "val", "storage_classes")
if err != nil {
return nil, fmt.Errorf("unable to set storage classes to pool placement %q for zone %q: %w", placementID, name, err)
}
inConfig[placementID] = struct{}{}
}
}
if len(idxToRemove) != 0 {
//delete placements from slice
updated := make([]interface{}, 0, len(placements)-len(idxToRemove))
for i := range placements {
if _, ok := idxToRemove[i]; ok {
//remove
continue
}
updated = append(updated, placements[i])
}
placements = updated
}
// add new placements from spec:
for placementID, p := range fromSpec {
if _, ok := inConfig[placementID]; ok {
//already in config
continue
}
pObj, err := toObj(p)
if err != nil {
return nil, fmt.Errorf("unable convert pool placement %q for zone %q: %w", placementID, name, err)
}
placements = append(placements, pObj)
}
// sort placements array.
// Reason: 'radosgw-admin zone set --infile' sorts placement_pools by key before storing it in ceph
// and returns JSON with sorted placement_pools array. So we sort input array for easy comparison with applied JSON.
sort.Slice(placements, func(i, j int) bool {
pI, ok := placements[i].(map[string]interface{})
if !ok {
return false
}
nameI, err := getObjProperty[string](pI, "key")
if err != nil {
return false
}
pJ, ok := placements[j].(map[string]interface{})
if !ok {
return false
}
nameJ, err := getObjProperty[string](pJ, "key")
if err != nil {
return false
}
return nameI < nameJ
})
_, err = updateObjProperty(zone, placements, "placement_pools")
if err != nil {
return nil, fmt.Errorf("unable to set pool placements for zone %q: %w", name, err)
}
return zone, nil
}
func getDefaultPlacementName(spec cephv1.ObjectSharedPoolsSpec) string {
for _, p := range spec.PoolPlacements {
if p.Default {
return p.Name
}
}
return defaultPlacementCephConfigName
}
func getDefaultMetadataPool(spec cephv1.ObjectSharedPoolsSpec) string {
for _, p := range spec.PoolPlacements {
if p.Default {
return p.MetadataPoolName
}
}
return spec.MetadataPoolName
}
// toZonePlacementPools converts pool placement CRD definition to zone config json format structures
func toZonePlacementPools(spec cephv1.ObjectSharedPoolsSpec, ns string) map[string]ZonePlacementPool {
res := make(map[string]ZonePlacementPool, len(spec.PoolPlacements)+1)
// map sharedPools if presented:
if spec.DataPoolName != "" && spec.MetadataPoolName != "" {
res[defaultPlacementCephConfigName] = ZonePlacementPool{
Key: defaultPlacementCephConfigName,
Val: ZonePlacementPoolVal{
// The extra pool is for omap data for multi-part uploads, so we use
// the metadata pool instead of the data pool.
DataExtraPool: spec.MetadataPoolName + ":" + ns + ".buckets.non-ec",
IndexPool: spec.MetadataPoolName + ":" + ns + ".buckets.index",
StorageClasses: map[string]ZonePlacementStorageClass{
defaultPlacementStorageClass: {
DataPool: spec.DataPoolName + ":" + ns + ".buckets.data",
},
},
// Workaround: radosgw-admin set zone json command sets incorrect default value for placement inline_data field.
// So we should set default value (true) explicitly.
// See: https://tracker.ceph.com/issues/67933
InlineData: true,
},
}
}
// map pool placements:
for _, pp := range spec.PoolPlacements {
res[pp.Name] = toZonePlacementPool(pp, ns)
}
return res
}
func toZonePlacementPool(spec cephv1.PoolPlacementSpec, ns string) ZonePlacementPool {
placementNS := ns + "." + spec.Name
// The extra pool is for omap data for multi-part uploads, so we use
// the metadata pool instead of the data pool.
nonECPool := spec.MetadataPoolName + ":" + placementNS + ".data.non-ec"
if spec.DataNonECPoolName != "" {
nonECPool = spec.DataNonECPoolName + ":" + placementNS + ".data.non-ec"
}
res := ZonePlacementPool{
Key: spec.Name,
Val: ZonePlacementPoolVal{
DataExtraPool: nonECPool,
IndexPool: spec.MetadataPoolName + ":" + placementNS + ".index",
StorageClasses: map[string]ZonePlacementStorageClass{
defaultPlacementStorageClass: {
DataPool: spec.DataPoolName + ":" + placementNS + ".data",
},
},
// Workaround: 'radosgw-admin set zone json' command sets incorrect default value for placement inline_data field.
// So we should set default value (true) explicitly.
// See: https://tracker.ceph.com/issues/67933
InlineData: true,
},
}
for _, v := range spec.StorageClasses {
res.Val.StorageClasses[v.Name] = ZonePlacementStorageClass{
DataPool: v.DataPoolName + ":" + ns + "." + v.Name,
}
}
return res
}
func adjustZoneGroupPlacementTargets(group, zone map[string]interface{}, defaultPlacement string) (map[string]interface{}, error) {
name, err := getObjProperty[string](group, "name")
if err != nil {
return nil, fmt.Errorf("unable to get zonegroup name: %w", err)
}
//deep copy source group
group, err = deepCopyJson(group)
if err != nil {
return nil, fmt.Errorf("unable to deep copy config for zonegroup %s: %w", name, err)
}
_, err = updateObjProperty(group, defaultPlacement, "default_placement")
if err != nil {
return nil, fmt.Errorf("unable to set default_placement for zonegroup %s: %w", name, err)
}
desiredTargets, err := createPlacementTargetsFromZonePoolPlacements(zone)
if err != nil {
return nil, fmt.Errorf("unable to create targets from placements for zonegroup %q: %w", name, err)
}
currentTargets, err := getObjProperty[[]interface{}](group, "placement_targets")
if err != nil {
return nil, fmt.Errorf("unable to get targets from placements for zonegroup %q: %w", name, err)
}
applied := map[string]struct{}{}
idxToRemove := map[int]struct{}{}
for i, target := range currentTargets {
tObj, ok := target.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unable to cast placement target to object for zonegroup %q: %+v", name, target)
}
tName, err := getObjProperty[string](tObj, "name")
if err != nil {
return nil, fmt.Errorf("unable to get placement target name for zonegroup %q: %w", name, err)
}
// update target:
if desired, ok := desiredTargets[tName]; ok {
sc := []interface{}{}
ok = castJson(desired.StorageClasses, &sc)
if ok {
_, err = updateObjProperty(tObj, sc, "storage_classes")
} else {
_, err = updateObjProperty(tObj, desired.StorageClasses, "storage_classes")
}
if err != nil {
return nil, fmt.Errorf("unable to set storage classes to pool placement target %q for zonegroup %q: %w", tName, name, err)
}
applied[tName] = struct{}{}
} else {
// remove target
idxToRemove[i] = struct{}{}
continue
}
}
if len(idxToRemove) != 0 {
//delete targets from slice
updated := make([]interface{}, 0, len(currentTargets)-len(idxToRemove))
for i := range currentTargets {
if _, ok := idxToRemove[i]; ok {
//remove
continue
}
updated = append(updated, currentTargets[i])
}
currentTargets = updated
}
// add new targets:
for targetName, target := range desiredTargets {
if _, ok := applied[targetName]; ok {
//already in config
continue
}
tObj, err := toObj(target)
if err != nil {
return nil, fmt.Errorf("unable convert placement target %q for zonegroup %q: %w", targetName, name, err)
}
currentTargets = append(currentTargets, tObj)
}
_, err = updateObjProperty(group, currentTargets, "placement_targets")
if err != nil {
return nil, fmt.Errorf("unable to set placement targets for zonegroup %q: %w", name, err)
}
return group, nil
}
func createPlacementTargetsFromZonePoolPlacements(zone map[string]interface{}) (map[string]ZonegroupPlacementTarget, error) {
zoneName, err := getObjProperty[string](zone, "name")
if err != nil {
return nil, fmt.Errorf("unable to get zone name: %w", err)
}
zonePoolPlacements, err := getObjProperty[[]interface{}](zone, "placement_pools")
if err != nil {
return nil, fmt.Errorf("unable to get pool placements for zone %q: %w", zoneName, err)
}
res := make(map[string]ZonegroupPlacementTarget, len(zonePoolPlacements))
for _, pp := range zonePoolPlacements {
ppObj, ok := pp.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unable to cast zone pool placement to json obj for zone %q: %+v", zoneName, pp)
}
name, err := getObjProperty[string](ppObj, "key")
if err != nil {
return nil, fmt.Errorf("unable to get pool placement key for zone %q: %w", zoneName, err)
}
storClasses, err := getObjProperty[map[string]interface{}](ppObj, "val", "storage_classes")
if err != nil {
return nil, fmt.Errorf("unable to get pool placement storage classes for zone %q: %w", zoneName, err)
}
target := ZonegroupPlacementTarget{
Name: name,
}
for sc := range storClasses {
target.StorageClasses = append(target.StorageClasses, sc)
}
sort.Strings(target.StorageClasses)
res[name] = target
}
return res, nil
}
func getZoneJSON(objContext *Context) (map[string]interface{}, error) {
if objContext.Realm == "" {
return nil, fmt.Errorf("get zone: object store realm is missing from context")
}
realmArg := fmt.Sprintf("--rgw-realm=%s", objContext.Realm)
if objContext.Zone == "" {
return nil, fmt.Errorf("get zone: object store zone is missing from context")
}
zoneArg := fmt.Sprintf("--rgw-zone=%s", objContext.Zone)
logger.Debugf("get zone: rgw-realm=%s, rgw-zone=%s", objContext.Realm, objContext.Zone)
jsonStr, err := RunAdminCommandNoMultisite(objContext, true, "zone", "get", realmArg, zoneArg)
if err != nil {
// This handles the case where the pod we use to exec command (act as a proxy) is not found/ready yet
// The caller can nicely handle the error and not overflow the op logs with misleading error messages
if kerrors.IsNotFound(err) {
return nil, err
}
return nil, errors.Wrap(err, "failed to get rgw zone group")
}
logger.Debugf("get zone success: rgw-realm=%s, rgw-zone=%s, res=%s", objContext.Realm, objContext.Zone, jsonStr)
res := map[string]interface{}{}
return res, json.Unmarshal([]byte(jsonStr), &res)
}
func getZoneGroupJSON(objContext *Context) (map[string]interface{}, error) {
if objContext.Realm == "" {
return nil, fmt.Errorf("get zonegroup: object store realm is missing from context")
}
realmArg := fmt.Sprintf("--rgw-realm=%s", objContext.Realm)
if objContext.Zone == "" {
return nil, fmt.Errorf("get zonegroup: object store zone is missing from context")
}
zoneArg := fmt.Sprintf("--rgw-zone=%s", objContext.Zone)
if objContext.ZoneGroup == "" {
return nil, fmt.Errorf("get zonegroup: object store zonegroup is missing from context")
}
zoneGroupArg := fmt.Sprintf("--rgw-zonegroup=%s", objContext.ZoneGroup)
logger.Debugf("get zonegroup: rgw-realm=%s, rgw-zone=%s, rgw-zonegroup=%s", objContext.Realm, objContext.Zone, objContext.ZoneGroup)
jsonStr, err := RunAdminCommandNoMultisite(objContext, true, "zonegroup", "get", realmArg, zoneGroupArg, zoneArg)
if err != nil {
// This handles the case where the pod we use to exec command (act as a proxy) is not found/ready yet
// The caller can nicely handle the error and not overflow the op logs with misleading error messages
if kerrors.IsNotFound(err) {
return nil, err
}
return nil, errors.Wrap(err, "failed to get rgw zone group")
}
logger.Debugf("get zonegroup success: rgw-realm=%s, rgw-zone=%s, rgw-zonegroup=%s, res=%s", objContext.Realm, objContext.Zone, objContext.ZoneGroup, jsonStr)
res := map[string]interface{}{}
return res, json.Unmarshal([]byte(jsonStr), &res)
}
func updateZoneJSON(objContext *Context, zone map[string]interface{}) (map[string]interface{}, error) {
if objContext.Realm == "" {
return nil, fmt.Errorf("update zone: object store realm is missing from context")
}
realmArg := fmt.Sprintf("--rgw-realm=%s", objContext.Realm)
if objContext.Zone == "" {
return nil, fmt.Errorf("update zone: object store zone is missing from context")
}
zoneArg := fmt.Sprintf("--rgw-zone=%s", objContext.Zone)
configBytes, err := json.Marshal(zone)
if err != nil {
return nil, err
}
configFilename := path.Join(objContext.Context.ConfigDir, objContext.Name+".zonecfg")
if err := os.WriteFile(configFilename, configBytes, 0600); err != nil {
return nil, errors.Wrap(err, "failed to write zone config file")
}
defer os.Remove(configFilename)
args := []string{"zone", "set", zoneArg, "--infile=" + configFilename, realmArg}
updatedBytes, err := RunAdminCommandNoMultisite(objContext, false, args...)
if err != nil {
return nil, errors.Wrap(err, "failed to set zone config")
}
logger.Debugf("update zone: %s json config updated value from %q to %q", objContext.Zone, string(configBytes), string(updatedBytes))
updated := map[string]interface{}{}
err = json.Unmarshal([]byte(updatedBytes), &updated)
return updated, err
}
func updateZoneGroupJSON(objContext *Context, group map[string]interface{}) (map[string]interface{}, error) {
if objContext.Realm == "" {
return nil, fmt.Errorf("update zonegroup: object store realm is missing from context")
}
realmArg := fmt.Sprintf("--rgw-realm=%s", objContext.Realm)
if objContext.Zone == "" {
return nil, fmt.Errorf("update zonegroup: object store zone is missing from context")
}
zoneArg := fmt.Sprintf("--rgw-zone=%s", objContext.Zone)
if objContext.ZoneGroup == "" {
return nil, fmt.Errorf("update zonegroup: object store zonegroup is missing from context")
}
zoneGroupArg := fmt.Sprintf("--rgw-zonegroup=%s", objContext.ZoneGroup)
configBytes, err := json.Marshal(group)
if err != nil {
return nil, err
}
configFilename := path.Join(objContext.Context.ConfigDir, objContext.Name+".zonegroupcfg")
if err := os.WriteFile(configFilename, configBytes, 0600); err != nil {
return nil, errors.Wrap(err, "failed to write zonegroup config file")
}
defer os.Remove(configFilename)
args := []string{"zonegroup", "set", zoneArg, "--infile=" + configFilename, realmArg, zoneGroupArg}
updatedBytes, err := RunAdminCommandNoMultisite(objContext, false, args...)
if err != nil {
return nil, errors.Wrap(err, fmt.Sprintf("failed to set zonegroup config %s", updatedBytes))
}
updated := map[string]interface{}{}
err = json.Unmarshal([]byte(updatedBytes), &updated)
return updated, err
}
type ZonegroupPlacementTarget struct {
Name string `json:"name"`
StorageClasses []string `json:"storage_classes"`
}
type ZonePlacementPool struct {
Key string `json:"key"`
Val ZonePlacementPoolVal `json:"val"`
}
type ZonePlacementPoolVal struct {
DataExtraPool string `json:"data_extra_pool"`
IndexPool string `json:"index_pool"`
StorageClasses map[string]ZonePlacementStorageClass `json:"storage_classes"`
InlineData bool `json:"inline_data"`
}
type ZonePlacementStorageClass struct {
DataPool string `json:"data_pool"`
}