From a0b1fd8b977f92a8ccdc34feef11d0575281a7ce Mon Sep 17 00:00:00 2001 From: Srinivas Pothuraju Date: Tue, 22 Oct 2024 18:41:20 -0500 Subject: [PATCH] Support Failover, Restart, Pause and Resume --- cmd/cluster/dr/failover_dr.go | 129 ++++++++++++++++++++++++++++++++++ cmd/cluster/dr/pause_dr.go | 95 +++++++++++++++++++++++++ cmd/cluster/dr/restart_dr.go | 116 ++++++++++++++++++++++++++++++ cmd/cluster/dr/resume_dr.go | 91 ++++++++++++++++++++++++ internal/client/client.go | 16 +++++ 5 files changed, 447 insertions(+) create mode 100644 cmd/cluster/dr/failover_dr.go create mode 100644 cmd/cluster/dr/pause_dr.go create mode 100644 cmd/cluster/dr/restart_dr.go create mode 100644 cmd/cluster/dr/resume_dr.go diff --git a/cmd/cluster/dr/failover_dr.go b/cmd/cluster/dr/failover_dr.go new file mode 100644 index 0000000..7b52183 --- /dev/null +++ b/cmd/cluster/dr/failover_dr.go @@ -0,0 +1,129 @@ +// Licensed to Yugabyte, Inc. under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. Yugabyte +// licenses this file to you 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 dr + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client" + "github.com/yugabyte/ybm-cli/internal/formatter" + ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +var failoverDrCmd = &cobra.Command{ + Use: "failover", + Short: "Failover DR for a cluster", + Long: `Failover DR for a cluster`, + Run: func(cmd *cobra.Command, args []string) { + authApi, err := ybmAuthClient.NewAuthApiClient() + if err != nil { + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + authApi.GetInfo("", "") + + drName, _ := cmd.Flags().GetString("dr-name") + safetimes, _ := cmd.Flags().GetStringArray("safetimes") + clusterId, err := authApi.GetClusterIdByName(ClusterName) + if err != nil { + logrus.Fatalf("Could not get cluster data: %s", ybmAuthClient.GetApiErrorDetails(err)) + } + drId, err := authApi.GetDrIdByName(clusterId, drName) + if err != nil { + logrus.Fatal(err) + } + namespacesResp, r, err := authApi.GetClusterNamespaces(clusterId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + dbNameToIdMap := map[string]string{} + for _, namespace := range namespacesResp.Data { + dbNameToIdMap[namespace.GetName()] = namespace.GetId() + } + + safetimesMap := map[string]int64{} + for _, safetimesString := range safetimes { + for _, safetime := range strings.Split(safetimesString, ",") { + kvp := strings.Split(safetime, "=") + if len(kvp) != 2 { + logrus.Fatalln("Incorrect format in safetime") + } + database := kvp[0] + if databaseId, exists := dbNameToIdMap[database]; exists { + safetimeInMinString := kvp[1] + safetimeInMin, err := strconv.Atoi(safetimeInMinString) + if err != nil { + logrus.Fatalln("Error:", err) + } + safetimesMap[databaseId] = int64(safetimeInMin) + } else { + logrus.Fatalf("The database %s doesn't exist", database) + } + } + } + + drFailoverRequest := ybmclient.NewDrFailoverRequestWithDefaults() + if len(safetimes) != 0 { + drFailoverRequest.SetNamespaceSafeTimes(safetimesMap) + } + response, err := authApi.FailoverXClusterDr(clusterId, drId).DrFailoverRequest(*drFailoverRequest).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", response) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + + msg := fmt.Sprintf("Failover is in progress for the DR %s ", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + if viper.GetBool("wait") { + returnStatus, err := authApi.WaitForTaskCompletion(clusterId, ybmclient.ENTITYTYPEENUM_CLUSTER, ybmclient.TASKTYPEENUM_DR_FAILOVER, []string{"FAILED", "SUCCEEDED"}, msg) + if err != nil { + logrus.Fatalf("error when getting task status: %s", err) + } + if returnStatus != "SUCCEEDED" { + logrus.Fatalf("Operation failed with error: %s", returnStatus) + } + fmt.Printf("Failover for DR config %s is successful\n", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + drGetResp, r, err := authApi.GetXClusterDr(clusterId, drId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + drCtx := formatter.Context{ + Output: os.Stdout, + Format: formatter.NewDrFormat(viper.GetString("output")), + } + + formatter.DrWrite(drCtx, []ybmclient.XClusterDrData{drGetResp.GetData()}, *authApi) + } else { + fmt.Println(msg) + } + + }, +} + +func init() { + DrCmd.AddCommand(failoverDrCmd) + failoverDrCmd.Flags().String("dr-name", "", "[REQUIRED] Name of the DR configuration.") + failoverDrCmd.MarkFlagRequired("dr-name") + failoverDrCmd.Flags().StringArray("safetimes", []string{}, "[OPTIONAL] Safetimes of the DR configuation. Please provide key value pairs =,=.") +} diff --git a/cmd/cluster/dr/pause_dr.go b/cmd/cluster/dr/pause_dr.go new file mode 100644 index 0000000..8961aa4 --- /dev/null +++ b/cmd/cluster/dr/pause_dr.go @@ -0,0 +1,95 @@ +// Licensed to Yugabyte, Inc. under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. Yugabyte +// licenses this file to you 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 dr + +import ( + "fmt" + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client" + "github.com/yugabyte/ybm-cli/internal/formatter" + ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +var pauseDrCmd = &cobra.Command{ + Use: "pause", + Short: "Pause DR for a cluster", + Long: `Pause DR for a cluster`, + Run: func(cmd *cobra.Command, args []string) { + authApi, err := ybmAuthClient.NewAuthApiClient() + if err != nil { + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + authApi.GetInfo("", "") + + drName, _ := cmd.Flags().GetString("dr-name") + durationInMin, _ := cmd.Flags().GetInt32("duration") + clusterId, err := authApi.GetClusterIdByName(ClusterName) + if err != nil { + logrus.Fatalf("Could not get cluster data: %s", ybmAuthClient.GetApiErrorDetails(err)) + } + drId, err := authApi.GetDrIdByName(clusterId, drName) + if err != nil { + logrus.Fatal(err) + } + + pauseDrRequest := ybmclient.NewPauseDrRequestWithDefaults() + pauseDrRequest.SetDurationMinutes(durationInMin) + response, err := authApi.PauseXClusterDr(clusterId, drId).PauseDrRequest(*pauseDrRequest).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", response) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + + msg := fmt.Sprintf("DR config %s is being paused", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + if viper.GetBool("wait") { + returnStatus, err := authApi.WaitForTaskCompletion(clusterId, ybmclient.ENTITYTYPEENUM_CLUSTER, ybmclient.TASKTYPEENUM_DR_PAUSE, []string{"FAILED", "SUCCEEDED"}, msg) + if err != nil { + logrus.Fatalf("error when getting task status: %s", err) + } + if returnStatus != "SUCCEEDED" { + logrus.Fatalf("Operation failed with error: %s", returnStatus) + } + fmt.Printf("DR config %s is paused successfully\n", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + drGetResp, r, err := authApi.GetXClusterDr(clusterId, drId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + drCtx := formatter.Context{ + Output: os.Stdout, + Format: formatter.NewDrFormat(viper.GetString("output")), + } + + formatter.DrWrite(drCtx, []ybmclient.XClusterDrData{drGetResp.GetData()}, *authApi) + } else { + fmt.Println(msg) + } + + }, +} + +func init() { + DrCmd.AddCommand(pauseDrCmd) + pauseDrCmd.Flags().String("dr-name", "", "[REQUIRED] Name of the DR configuration.") + pauseDrCmd.MarkFlagRequired("dr-name") + pauseDrCmd.Flags().Int32("duration", 60, "[OPTIONAL] Duration in minutes.") +} diff --git a/cmd/cluster/dr/restart_dr.go b/cmd/cluster/dr/restart_dr.go new file mode 100644 index 0000000..feead5d --- /dev/null +++ b/cmd/cluster/dr/restart_dr.go @@ -0,0 +1,116 @@ +// Licensed to Yugabyte, Inc. under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. Yugabyte +// licenses this file to you 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 dr + +import ( + "fmt" + "os" + "strings" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client" + "github.com/yugabyte/ybm-cli/internal/formatter" + ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +var restartDrCmd = &cobra.Command{ + Use: "restart", + Short: "Restart DR for a cluster", + Long: `Restart DR for a cluster`, + Run: func(cmd *cobra.Command, args []string) { + authApi, err := ybmAuthClient.NewAuthApiClient() + if err != nil { + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + authApi.GetInfo("", "") + + drName, _ := cmd.Flags().GetString("dr-name") + databases, _ := cmd.Flags().GetStringArray("databases") + clusterId, err := authApi.GetClusterIdByName(ClusterName) + if err != nil { + logrus.Fatalf("Could not get cluster data: %s", ybmAuthClient.GetApiErrorDetails(err)) + } + drId, err := authApi.GetDrIdByName(clusterId, drName) + if err != nil { + logrus.Fatal(err) + } + namespacesResp, r, err := authApi.GetClusterNamespaces(clusterId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + dbNameToIdMap := map[string]string{} + for _, namespace := range namespacesResp.Data { + dbNameToIdMap[namespace.GetName()] = namespace.GetId() + } + databaseIds := []string{} + for _, databaseString := range databases { + for _, database := range strings.Split(databaseString, ",") { + if databaseId, exists := dbNameToIdMap[database]; exists { + databaseIds = append(databaseIds, databaseId) + } else { + logrus.Fatalf("The database %s doesn't exist", database) + } + } + } + restartDrRequest := ybmclient.NewDrRestartRequestWithDefaults() + if len(databaseIds) != 0 { + restartDrRequest.SetDatabaseIds(databaseIds) + } + response, err := authApi.RestartXClusterDr(clusterId, drId).DrRestartRequest(*restartDrRequest).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", response) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + + msg := fmt.Sprintf("DR config %s is being restartd", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + if viper.GetBool("wait") { + returnStatus, err := authApi.WaitForTaskCompletion(clusterId, ybmclient.ENTITYTYPEENUM_CLUSTER, ybmclient.TASKTYPEENUM_DR_RESTART, []string{"FAILED", "SUCCEEDED"}, msg) + if err != nil { + logrus.Fatalf("error when getting task status: %s", err) + } + if returnStatus != "SUCCEEDED" { + logrus.Fatalf("Operation failed with error: %s", returnStatus) + } + fmt.Printf("DR config %s is restartd successfully\n", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + drGetResp, r, err := authApi.GetXClusterDr(clusterId, drId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + drCtx := formatter.Context{ + Output: os.Stdout, + Format: formatter.NewDrFormat(viper.GetString("output")), + } + + formatter.DrWrite(drCtx, []ybmclient.XClusterDrData{drGetResp.GetData()}, *authApi) + } else { + fmt.Println(msg) + } + + }, +} + +func init() { + DrCmd.AddCommand(restartDrCmd) + restartDrCmd.Flags().String("dr-name", "", "[REQUIRED] Name of the DR configuration.") + restartDrCmd.MarkFlagRequired("dr-name") + restartDrCmd.Flags().StringArray("databases", []string{}, "[OPTIONAL] Databases to be restarted.") +} diff --git a/cmd/cluster/dr/resume_dr.go b/cmd/cluster/dr/resume_dr.go new file mode 100644 index 0000000..eff1019 --- /dev/null +++ b/cmd/cluster/dr/resume_dr.go @@ -0,0 +1,91 @@ +// Licensed to Yugabyte, Inc. under one or more contributor license +// agreements. See the NOTICE file distributed with this work for +// additional information regarding copyright ownership. Yugabyte +// licenses this file to you 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 dr + +import ( + "fmt" + "os" + + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "github.com/spf13/viper" + ybmAuthClient "github.com/yugabyte/ybm-cli/internal/client" + "github.com/yugabyte/ybm-cli/internal/formatter" + ybmclient "github.com/yugabyte/yugabytedb-managed-go-client-internal" +) + +var resumeDrCmd = &cobra.Command{ + Use: "resume", + Short: "Resume DR for a cluster", + Long: `Resume DR for a cluster`, + Run: func(cmd *cobra.Command, args []string) { + authApi, err := ybmAuthClient.NewAuthApiClient() + if err != nil { + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + authApi.GetInfo("", "") + + drName, _ := cmd.Flags().GetString("dr-name") + clusterId, err := authApi.GetClusterIdByName(ClusterName) + if err != nil { + logrus.Fatalf("Could not get cluster data: %s", ybmAuthClient.GetApiErrorDetails(err)) + } + drId, err := authApi.GetDrIdByName(clusterId, drName) + if err != nil { + logrus.Fatal(err) + } + + response, err := authApi.ResumeXClusterDr(clusterId, drId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", response) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + + msg := fmt.Sprintf("DR config %s is being resumed", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + if viper.GetBool("wait") { + returnStatus, err := authApi.WaitForTaskCompletion(clusterId, ybmclient.ENTITYTYPEENUM_CLUSTER, ybmclient.TASKTYPEENUM_DR_RESUME, []string{"FAILED", "SUCCEEDED"}, msg) + if err != nil { + logrus.Fatalf("error when getting task status: %s", err) + } + if returnStatus != "SUCCEEDED" { + logrus.Fatalf("Operation failed with error: %s", returnStatus) + } + fmt.Printf("DR config %s is resumed successful\n", formatter.Colorize(drName, formatter.GREEN_COLOR)) + + drGetResp, r, err := authApi.GetXClusterDr(clusterId, drId).Execute() + if err != nil { + logrus.Debugf("Full HTTP response: %v", r) + logrus.Fatalf(ybmAuthClient.GetApiErrorDetails(err)) + } + drCtx := formatter.Context{ + Output: os.Stdout, + Format: formatter.NewDrFormat(viper.GetString("output")), + } + + formatter.DrWrite(drCtx, []ybmclient.XClusterDrData{drGetResp.GetData()}, *authApi) + } else { + fmt.Println(msg) + } + + }, +} + +func init() { + DrCmd.AddCommand(resumeDrCmd) + resumeDrCmd.Flags().String("dr-name", "", "[REQUIRED] Name of the DR configuration.") + resumeDrCmd.MarkFlagRequired("dr-name") +} diff --git a/internal/client/client.go b/internal/client/client.go index ba3f20f..626b793 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1682,6 +1682,22 @@ func (a *AuthApiClient) SwitchoverXClusterDr(clusterId string, drId string) ybmc return a.ApiClient.XclusterDrApi.Switchover(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) } +func (a *AuthApiClient) FailoverXClusterDr(clusterId string, drId string) ybmclient.ApiFailoverRequest { + return a.ApiClient.XclusterDrApi.Failover(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) +} + +func (a *AuthApiClient) PauseXClusterDr(clusterId string, drId string) ybmclient.ApiPauseRequest { + return a.ApiClient.XclusterDrApi.Pause(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) +} + +func (a *AuthApiClient) RestartXClusterDr(clusterId string, drId string) ybmclient.ApiRestartRequest { + return a.ApiClient.XclusterDrApi.Restart(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) +} + +func (a *AuthApiClient) ResumeXClusterDr(clusterId string, drId string) ybmclient.ApiResumeRequest { + return a.ApiClient.XclusterDrApi.Resume(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) +} + func (a *AuthApiClient) GetXClusterDr(clusterId string, drId string) ybmclient.ApiGetXClusterDrRequest { return a.ApiClient.XclusterDrApi.GetXClusterDr(a.ctx, a.AccountID, a.ProjectID, clusterId, drId) }