-
Notifications
You must be signed in to change notification settings - Fork 72
feat: add radosgw-admin multisite read wrappers #809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
johnramsden
wants to merge
4
commits into
canonical:main
Choose a base branch
from
johnramsden:feat/rgw-multisite-read-wrappers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from 1 commit
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
8d2fb4c
feat: add radosgw-admin multisite read wrappers
johnramsden 7226606
refactor: use typed enum for RGW metadata sync state
johnramsden 01f57d7
fix: fold NumShards into RGW sync verdict CaughtUp calculation
johnramsden 91bab6c
fix: validate RGW sync status shard invariants before returning
johnramsden File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,342 @@ | ||
| package ceph | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
|
|
||
| "github.com/canonical/microceph/microceph/common" | ||
| "github.com/canonical/microceph/microceph/logger" | ||
| ) | ||
|
|
||
| // radosgwAdminRun runs radosgw-admin with the given arguments. | ||
| func radosgwAdminRun(args ...string) (string, error) { | ||
| return common.ProcessExec.RunCommand("radosgw-admin", args...) | ||
| } | ||
|
|
||
| // radosgwAdminRunRemote runs radosgw-admin against an imported remote | ||
| // cluster when the cluster/client pair is non-empty, and locally otherwise. | ||
| func radosgwAdminRunRemote(cluster string, client string, args ...string) (string, error) { | ||
| args = appendRemoteClusterArgs(args, cluster, client) | ||
| return radosgwAdminRun(args...) | ||
| } | ||
|
|
||
| // RgwRealm holds the subset of `radosgw-admin realm get` output consumed by | ||
| // RGW replication. | ||
| type RgwRealm struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| CurrentPeriod string `json:"current_period"` | ||
| Epoch int `json:"epoch"` | ||
| } | ||
|
|
||
| // RgwZoneGroupZone is one zone entry in a zonegroup map. | ||
| type RgwZoneGroupZone struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| Endpoints []string `json:"endpoints"` | ||
| ReadOnly bool `json:"read_only"` | ||
| } | ||
|
|
||
| // RgwZoneGroup holds the subset of `radosgw-admin zonegroup get` output | ||
| // consumed by RGW replication. | ||
| type RgwZoneGroup struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| IsMaster bool `json:"is_master"` | ||
| Endpoints []string `json:"endpoints"` | ||
| MasterZone string `json:"master_zone"` | ||
| Zones []RgwZoneGroupZone `json:"zones"` | ||
| RealmID string `json:"realm_id"` | ||
| } | ||
|
|
||
| // RgwZoneSystemKey is the S3 key pair bound to a zone for inter-zone sync. | ||
| type RgwZoneSystemKey struct { | ||
| AccessKey string `json:"access_key"` | ||
| SecretKey string `json:"secret_key"` | ||
| } | ||
|
|
||
| // RgwZone holds the subset of `radosgw-admin zone get` output consumed by | ||
| // RGW replication. | ||
| type RgwZone struct { | ||
| ID string `json:"id"` | ||
| Name string `json:"name"` | ||
| SystemKey RgwZoneSystemKey `json:"system_key"` | ||
| } | ||
|
|
||
| // GetRgwRealm fetches the default realm. Pass a non-empty cluster/client | ||
| // pair to target an imported remote cluster instead of the local one. | ||
| // A failing command (e.g. no realm configured) yields a zero-value realm | ||
| // with a nil error | ||
| func GetRgwRealm(cluster string, client string) (RgwRealm, error) { | ||
| response := RgwRealm{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "realm", "get") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed realm get operation: %v", err) | ||
| return response, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &response) | ||
| if err != nil { | ||
| return response, fmt.Errorf("cannot unmarshal realm get output: %w", err) | ||
| } | ||
|
|
||
| return response, nil | ||
| } | ||
|
|
||
| // GetRgwZoneGroup fetches the default zonegroup. Pass a non-empty | ||
| // cluster/client pair to target an imported remote cluster instead of the | ||
| // local one. A failing command yields a zero-value zonegroup with a nil | ||
| // error | ||
| func GetRgwZoneGroup(cluster string, client string) (RgwZoneGroup, error) { | ||
| response := RgwZoneGroup{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "zonegroup", "get") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed zonegroup get operation: %v", err) | ||
| return response, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &response) | ||
| if err != nil { | ||
| return response, fmt.Errorf("cannot unmarshal zonegroup get output: %w", err) | ||
| } | ||
|
|
||
| return response, nil | ||
| } | ||
|
|
||
| // GetRgwZone fetches the default zone. Pass a non-empty cluster/client pair | ||
| // to target an imported remote cluster instead of the local one. A failing | ||
| // command yields a zero-value zone with a nil error | ||
| func GetRgwZone(cluster string, client string) (RgwZone, error) { | ||
| response := RgwZone{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "zone", "get") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed zone get operation: %v", err) | ||
| return response, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &response) | ||
| if err != nil { | ||
| return response, fmt.Errorf("cannot unmarshal zone get output: %w", err) | ||
| } | ||
|
|
||
| return response, nil | ||
| } | ||
|
|
||
| // RgwSyncInfo is the info block shared by `metadata sync status` and | ||
| // `data sync status` output. Period and RealmEpoch are metadata-only. | ||
| type RgwSyncInfo struct { | ||
| Status string `json:"status"` | ||
| NumShards int `json:"num_shards"` | ||
| Period string `json:"period"` | ||
| RealmEpoch int `json:"realm_epoch"` | ||
| } | ||
|
|
||
| // RgwMetadataSyncMarker is one shard's metadata sync position. State is | ||
| // numeric in this output: 0 = full sync, 1 = incremental sync. | ||
| type RgwMetadataSyncMarker struct { | ||
| State int `json:"state"` | ||
| Marker string `json:"marker"` | ||
| } | ||
|
|
||
| // RgwMetadataSyncShard pairs a shard id with its metadata sync marker. | ||
| type RgwMetadataSyncShard struct { | ||
| Key int `json:"key"` | ||
| Val RgwMetadataSyncMarker `json:"val"` | ||
| } | ||
|
|
||
| // RgwMetadataSyncStatus is the parsed form of `radosgw-admin metadata sync | ||
| // status`. On the metadata master the command reports Status "init" with | ||
| // zero shards - the master runs no metadata sync. | ||
| type RgwMetadataSyncStatus struct { | ||
| Info RgwSyncInfo | ||
| Markers []RgwMetadataSyncShard | ||
| } | ||
|
|
||
| // RgwDataSyncMarker is one shard's data sync position. Unlike the metadata | ||
| // variant, state is a string here: "full-sync" or "incremental-sync". | ||
| type RgwDataSyncMarker struct { | ||
| Status string `json:"status"` | ||
| Marker string `json:"marker"` | ||
| } | ||
|
|
||
| // RgwDataSyncShard pairs a shard id with its data sync marker. | ||
| type RgwDataSyncShard struct { | ||
| Key int `json:"key"` | ||
| Val RgwDataSyncMarker `json:"val"` | ||
| } | ||
|
|
||
| // RgwDataSyncStatus is the parsed form of `radosgw-admin data sync status | ||
| // --source-zone=<zone>` for one sync source. | ||
| type RgwDataSyncStatus struct { | ||
| Info RgwSyncInfo | ||
| Markers []RgwDataSyncShard | ||
| } | ||
|
|
||
| type rgwMetadataSyncEnvelope struct { | ||
| SyncStatus struct { | ||
| Info RgwSyncInfo `json:"info"` | ||
| Markers []RgwMetadataSyncShard `json:"markers"` | ||
| } `json:"sync_status"` | ||
| } | ||
|
|
||
| type rgwDataSyncEnvelope struct { | ||
| SyncStatus struct { | ||
| Info RgwSyncInfo `json:"info"` | ||
| Markers []RgwDataSyncShard `json:"markers"` | ||
| } `json:"sync_status"` | ||
| } | ||
|
|
||
| // GetRgwMetadataSyncStatus fetches the typed metadata sync state (JSON, | ||
| // local sync markers only - no peer comparison). Pass a non-empty | ||
| // cluster/client pair to target an imported remote cluster. A failing | ||
| // command yields a zero-value status with a nil error | ||
| func GetRgwMetadataSyncStatus(cluster string, client string) (RgwMetadataSyncStatus, error) { | ||
| envelope := rgwMetadataSyncEnvelope{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "metadata", "sync", "status") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed metadata sync status operation: %v", err) | ||
| return RgwMetadataSyncStatus{}, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &envelope) | ||
| if err != nil { | ||
| return RgwMetadataSyncStatus{}, fmt.Errorf("cannot unmarshal metadata sync status output: %w", err) | ||
| } | ||
|
|
||
| return RgwMetadataSyncStatus{Info: envelope.SyncStatus.Info, Markers: envelope.SyncStatus.Markers}, nil | ||
| } | ||
|
|
||
| // GetRgwDataSyncStatus fetches the typed data sync state for one source | ||
| // zone (JSON, local sync markers only - no peer comparison). Pass a | ||
| // non-empty cluster/client pair to target an imported remote cluster. A | ||
| // failing command yields a zero-value status with a nil error, mirroring | ||
| // the RBD wrapper contract. | ||
| func GetRgwDataSyncStatus(sourceZone string, cluster string, client string) (RgwDataSyncStatus, error) { | ||
| envelope := rgwDataSyncEnvelope{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "data", "sync", "status", "--source-zone", sourceZone) | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed data sync status operation for source(%s): %v", sourceZone, err) | ||
| return RgwDataSyncStatus{}, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &envelope) | ||
| if err != nil { | ||
| return RgwDataSyncStatus{}, fmt.Errorf("cannot unmarshal data sync status output: %w", err) | ||
| } | ||
|
|
||
| return RgwDataSyncStatus{Info: envelope.SyncStatus.Info, Markers: envelope.SyncStatus.Markers}, nil | ||
|
johnramsden marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // RgwLogShard is one shard entry of `mdlog status` or `datalog status` | ||
| // output: the log head position on the cluster that owns the log. The | ||
| // array index is the shard id. | ||
| type RgwLogShard struct { | ||
| Marker string `json:"marker"` | ||
| LastUpdate string `json:"last_update"` | ||
| } | ||
|
|
||
| // GetRgwMdlogStatus fetches the metadata log head markers, one entry per | ||
| // shard. Run it against the metadata master (via the cluster/client pair) | ||
| // when computing a secondary's catch-up verdict. A failing command yields | ||
| // nil with a nil error | ||
| func GetRgwMdlogStatus(cluster string, client string) ([]RgwLogShard, error) { | ||
| shards := []RgwLogShard{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "mdlog", "status") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed mdlog status operation: %v", err) | ||
| return nil, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &shards) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cannot unmarshal mdlog status output: %w", err) | ||
| } | ||
|
|
||
| return shards, nil | ||
| } | ||
|
|
||
| // GetRgwDatalogStatus fetches the data log head markers, one entry per | ||
| // shard. Run it against the source zone's cluster (via the cluster/client | ||
| // pair) when computing the catch-up verdict for sync from that source. A | ||
| // failing command yields nil with a nil error | ||
| func GetRgwDatalogStatus(cluster string, client string) ([]RgwLogShard, error) { | ||
| shards := []RgwLogShard{} | ||
|
|
||
| output, err := radosgwAdminRunRemote(cluster, client, "datalog", "status") | ||
| if err != nil { | ||
| logger.Warnf("REPRGW: failed datalog status operation: %v", err) | ||
| return nil, nil | ||
| } | ||
|
|
||
| err = json.Unmarshal([]byte(output), &shards) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("cannot unmarshal datalog status output: %w", err) | ||
| } | ||
|
|
||
| return shards, nil | ||
| } | ||
|
|
||
| // RgwSyncVerdict is the deterministically computed catch-up verdict for one | ||
| // sync relationship: local sync markers compared against the peer's log | ||
| // heads, using the same per-shard rule radosgw-admin's own `sync status` | ||
| // applies (a shard still in full sync, or an incremental shard whose peer | ||
| // head is past the local marker, counts as behind). It omits upstream's | ||
| // entry-listing prune step, so a shard whose peer log was trimmed may | ||
| // transiently over-report as behind. | ||
| type RgwSyncVerdict struct { | ||
| CaughtUp bool | ||
| BehindShards []int | ||
| FullSyncShards int | ||
| PeriodMismatch bool | ||
| } | ||
|
|
||
| // ComputeRgwMetadataSyncVerdict compares a secondary's metadata sync | ||
| // markers with the master's mdlog heads. currentPeriod is the realm's | ||
| // current period id; a secondary syncing an older period is reported as | ||
| // PeriodMismatch without a per-shard comparison, as upstream does. | ||
| func ComputeRgwMetadataSyncVerdict(local RgwMetadataSyncStatus, masterLog []RgwLogShard, currentPeriod string) RgwSyncVerdict { | ||
| verdict := RgwSyncVerdict{} | ||
|
|
||
| if local.Info.Period != "" && currentPeriod != "" && local.Info.Period != currentPeriod { | ||
| verdict.PeriodMismatch = true | ||
| return verdict | ||
| } | ||
|
|
||
| for _, shard := range local.Markers { | ||
| if shard.Val.State != 1 { | ||
| verdict.FullSyncShards++ | ||
| continue | ||
| } | ||
| if shard.Key < len(masterLog) && masterLog[shard.Key].Marker > shard.Val.Marker { | ||
| verdict.BehindShards = append(verdict.BehindShards, shard.Key) | ||
| } | ||
| } | ||
|
|
||
| verdict.CaughtUp = len(verdict.BehindShards) == 0 && verdict.FullSyncShards == 0 | ||
| return verdict | ||
|
johnramsden marked this conversation as resolved.
|
||
| } | ||
|
|
||
| // ComputeRgwDataSyncVerdict compares local data sync markers for one source | ||
| // zone with that source's datalog heads. | ||
| func ComputeRgwDataSyncVerdict(local RgwDataSyncStatus, sourceLog []RgwLogShard) RgwSyncVerdict { | ||
| verdict := RgwSyncVerdict{} | ||
|
|
||
| for _, shard := range local.Markers { | ||
| if shard.Val.Status != "incremental-sync" { | ||
| verdict.FullSyncShards++ | ||
| continue | ||
| } | ||
| if shard.Key < len(sourceLog) && sourceLog[shard.Key].Marker > shard.Val.Marker { | ||
| verdict.BehindShards = append(verdict.BehindShards, shard.Key) | ||
| } | ||
| } | ||
|
|
||
| verdict.CaughtUp = len(verdict.BehindShards) == 0 && verdict.FullSyncShards == 0 | ||
| return verdict | ||
|
johnramsden marked this conversation as resolved.
|
||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.