oracle/pkg/agents/standby/standby.go (311 lines of code) (raw):
// Copyright 2022 Google LLC
//
// 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 standby
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/controllers/standbyhelpers"
connect "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/common"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/consts"
dbdpb "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/oracle"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/util/task"
lropb "google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var (
configBaseDir = fmt.Sprintf(consts.ConfigBaseDir, consts.DataMount)
)
// SecretAccessor defines the methods we use from the secret accessor.
type SecretAccessor interface {
Get(context.Context) (string, error)
}
// Primary is a domain object that describes an Oracle external primary
// instance.
type Primary struct {
Host string
Port int
Service string
User string
PasswordAccessor SecretAccessor
}
// Standby is a domain object that describes an Oracle standby replica
// instance.
type Standby struct {
CDBName string
DBUniqueName string
DBDomain string
Host string
Port int
LogDiskSize int64
Version string
}
// dgMembers describes members in DG configuration.
type dgMembers struct {
configuration string
primary string
physicalStandbys []string
logicalStandbys []string
}
// standbyContains returns whether the specified dbUniqueName is a member of
// physical or logical standby of the data guard configuration.
func (m *dgMembers) standbyContains(dbUniqueName string) bool {
for _, db := range m.physicalStandbys {
if strings.EqualFold(db, dbUniqueName) {
return true
}
}
for _, db := range m.logicalStandbys {
if strings.EqualFold(db, dbUniqueName) {
return true
}
}
return false
}
// size returns the total number of group members in the data guard configuration.
func (m *dgMembers) size() int {
return len(m.physicalStandbys) + len(m.logicalStandbys)
}
// CreateStandby creates a standby database by cloning a external database.
func CreateStandby(ctx context.Context, primary *Primary, standby *Standby, backupGcsPath, operationId string, dbdClient dbdpb.DatabaseDaemonClient) (*lropb.Operation, error) {
operation, err := dbdClient.GetOperation(ctx, &lropb.GetOperationRequest{Name: operationId})
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
t := newCreateStandbyTask(ctx, primary, standby, backupGcsPath, operationId, dbdClient)
err := task.Do(ctx, t.tasks)
if err != nil {
return nil, err
}
return t.lro, nil
} else if err != nil {
return nil, fmt.Errorf("CreateStandby: failed to GetOperation with err %v", err)
}
return operation, nil
}
// SetUpDataGuard sets up Data Guard between primary and standby.
func SetUpDataGuard(ctx context.Context, primary *Primary, standby *Standby, passwordFileGcsPath string, dbdClient dbdpb.DatabaseDaemonClient) error {
t := newSetUpStandbyTask(ctx, primary, standby, passwordFileGcsPath, dbdClient)
return task.Do(ctx, t.tasks)
}
// DataGuardStatus get configuration and this standby database status.
func DataGuardStatus(ctx context.Context, StandbyUniqueName string, dbdClient dbdpb.DatabaseDaemonClient) ([]string, error) {
resp, err := dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: "/",
Scripts: []string{
"show configuration",
fmt.Sprintf("show database %s", StandbyUniqueName),
},
})
if err != nil {
return nil, err
}
return resp.GetOutput(), err
}
// PromoteStandby promotes standby database to primary.
func PromoteStandby(ctx context.Context, primary *Primary, standby *Standby, dbdClient dbdpb.DatabaseDaemonClient) error {
t := newPromoteStandbyTask(ctx, primary, standby, dbdClient)
return task.Do(ctx, t.tasks)
}
// BootstrapStandby converts promoted standby to standard El Carro Oracle instance.
func BootstrapStandby(ctx context.Context, dbdClient dbdpb.DatabaseDaemonClient) error {
t := newBootstrapStandbyTask(ctx, dbdClient)
return task.Do(ctx, t.tasks)
}
// VerifyStandbySettings does preflight checks on standby settings.
func VerifyStandbySettings(ctx context.Context, primary *Primary, standby *Standby, passwordGcsPath, backupGcsPath string, dbdClient dbdpb.DatabaseDaemonClient) (settingErrs []*standbyhelpers.StandbySettingErr) {
t := newVerifyStandbySettingsTask(ctx, primary, standby, passwordGcsPath, backupGcsPath, dbdClient)
task.Do(ctx, t.tasks)
return t.settingErrs
}
type dgConfig struct {
dbdClient dbdpb.DatabaseDaemonClient
buildTarget func(ctx context.Context) (string, error)
configurationNameRe *regexp.Regexp
primaryUniqueNameRe *regexp.Regexp
physicalStandbyUniqueNameRe *regexp.Regexp
logicalStandbyUniqueNameRe *regexp.Regexp
connRe *regexp.Regexp
}
func (d *dgConfig) exists(ctx context.Context) bool {
target, err := d.buildTarget(ctx)
if err != nil {
return false
}
_, err = d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{"show configuration"},
})
return err == nil
}
func (d *dgConfig) remove(ctx context.Context) error {
target, err := d.buildTarget(ctx)
if err != nil {
return fmt.Errorf("failed to build target: %v", err)
}
if resp, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{"remove configuration"},
}); err != nil {
return fmt.Errorf("failed to remove configuration: %v, with response: %v", err, resp)
}
return nil
}
func (d *dgConfig) removeStandbyDB(ctx context.Context, dbUniqueName string) error {
target, err := d.buildTarget(ctx)
if err != nil {
return fmt.Errorf("failed to build target: %v", err)
}
if resp, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{fmt.Sprintf("disable database %s", dbUniqueName)},
}); err != nil {
return fmt.Errorf("failed to disable database: %v, with response: %v", err, resp)
}
if resp, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{fmt.Sprintf("remove database %s", dbUniqueName)},
}); err != nil {
return fmt.Errorf("failed to remove database: %v, with response: %v", err, resp)
}
return nil
}
func (d *dgConfig) members(ctx context.Context) (*dgMembers, error) {
target, err := d.buildTarget(ctx)
if err != nil {
return nil, fmt.Errorf("failed to build target: %v", err)
}
resp, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{"show configuration"},
})
if err != nil {
return nil, fmt.Errorf("failed to get DG configuration: %v", err)
}
if len(resp.GetOutput()) != 1 {
return nil, fmt.Errorf("got unexpected resp: %v, want len(resp.GetOuput()) = 1", resp)
}
config := d.configurationNameRe.FindStringSubmatch(resp.GetOutput()[0])
if config == nil {
return nil, fmt.Errorf("failed to find configuration name from %v", resp.GetOutput()[0])
}
pUnique := d.primaryUniqueNameRe.FindStringSubmatch(resp.GetOutput()[0])
if pUnique == nil {
return nil, fmt.Errorf("failed to find primary unique name from %v", resp.GetOutput()[0])
}
physicalUniques := d.physicalStandbyUniqueNameRe.FindAllStringSubmatch(resp.GetOutput()[0], -1)
var pStandbys []string
for _, physicalUnique := range physicalUniques {
pStandbys = append(pStandbys, physicalUnique[1])
}
logicalUniques := d.logicalStandbyUniqueNameRe.FindAllStringSubmatch(resp.GetOutput()[0], -1)
var lStandbys []string
for _, logicalUnique := range logicalUniques {
lStandbys = append(lStandbys, logicalUnique[1])
}
return &dgMembers{
configuration: config[1],
primary: pUnique[1],
physicalStandbys: pStandbys,
logicalStandbys: lStandbys,
}, nil
}
func (d *dgConfig) connectIdentifier(ctx context.Context, uniqueName string) (string, error) {
target, err := d.buildTarget(ctx)
if err != nil {
return "", fmt.Errorf("failed to build target: %v", err)
}
resp, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{fmt.Sprintf("show database %s dgconnectidentifier", uniqueName)},
})
if err != nil {
return "", fmt.Errorf("failed to get connect identifier: %v", err)
}
if len(resp.GetOutput()) != 1 {
return "", fmt.Errorf("got unexpected resp: %v, want len(resp.GetOuput()) = 1", resp)
}
matched := d.connRe.FindStringSubmatch(resp.GetOutput()[0])
if matched == nil {
return "", fmt.Errorf("failed to find connect identifier from %v", resp.GetOutput()[0])
}
return matched[1], nil
}
func (d *dgConfig) setConnectIdentifier(ctx context.Context, uniqueName, newIdentifier string) error {
target, err := d.buildTarget(ctx)
if err != nil {
return fmt.Errorf("failed to build target: %v", err)
}
if _, err := d.dbdClient.RunDataGuard(ctx, &dbdpb.RunDataGuardRequest{
Target: target,
Scripts: []string{fmt.Sprintf("edit database '%s' set property 'dgconnectidentifier'='%s'", uniqueName, newIdentifier)},
}); err != nil {
return fmt.Errorf("failed to update connect identifier(%s) for DB(%s) : %v", newIdentifier, uniqueName, err)
}
return nil
}
func newDgConfig(dbdClient dbdpb.DatabaseDaemonClient, buildTarget func(ctx context.Context) (string, error)) *dgConfig {
return &dgConfig{
dbdClient: dbdClient,
buildTarget: buildTarget,
configurationNameRe: regexp.MustCompile(`Configuration\s*-\s*(\S+)`),
primaryUniqueNameRe: regexp.MustCompile(`(\S+)\s*-\s*Primary database`),
physicalStandbyUniqueNameRe: regexp.MustCompile(`(\S+)\s*-\s*Physical standby database`),
logicalStandbyUniqueNameRe: regexp.MustCompile(`(\S+)\s*-\s*Logical standby database`),
connRe: regexp.MustCompile(`DGConnectIdentifier\s*=\s*'(\S+)'`),
}
}
func fetchAndParseSingleColumnMultiRowQueriesLocal(ctx context.Context, dbdClient dbdpb.DatabaseDaemonClient, query string) ([]string, error) {
res, err := fetchAndParseQueries(
ctx,
&dbdpb.RunSQLPlusCMDRequest{
Commands: []string{query},
ConnectInfo: &dbdpb.RunSQLPlusCMDRequest_Local{},
},
dbdClient,
)
if err != nil {
return nil, err
}
var rows []string
for _, row := range res {
if len(row) != 1 {
return nil, fmt.Errorf("fetchAndParseSingleColumnMultiRowQueriesLocal: # of cols returned by query != 1: %v", row)
}
for _, v := range row {
rows = append(rows, v)
}
}
return rows, nil
}
// fetchAndParseSingleColumnMultiRowQueriesFromEM is a utility method intended
// for running single column queries on the external server. It parses the
// single column JSON result-set (returned by runSQLPlus API) and returns a list.
func fetchAndParseSingleColumnMultiRowQueries(ctx context.Context, primary *Primary, dbdClient dbdpb.DatabaseDaemonClient, query string) ([]string, error) {
passwd, err := primary.PasswordAccessor.Get(ctx)
if err != nil {
return nil, err
}
res, err := fetchAndParseQueries(
ctx,
&dbdpb.RunSQLPlusCMDRequest{
Commands: []string{query},
Suppress: true,
ConnectInfo: &dbdpb.RunSQLPlusCMDRequest_Dsn{Dsn: connect.EZ(primary.User, passwd, primary.Host, strconv.Itoa(primary.Port), primary.Service, true)},
},
dbdClient,
)
if err != nil {
return nil, err
}
var rows []string
for _, row := range res {
if len(row) != 1 {
return nil, fmt.Errorf("fetchAndParseSingleColumnMultiRowQueries: # of cols returned by query != 1: %v", row)
}
for _, v := range row {
rows = append(rows, v)
}
}
return rows, nil
}
// fetchAndParseQueries is a utility method intended for running queries
// on the external server. It parses the JSON result-set (returned by runSQLPlus
// API) and returns a list of rows with column-value mapping.
func fetchAndParseQueries(ctx context.Context, sqlRequest *dbdpb.RunSQLPlusCMDRequest, dbdClient dbdpb.DatabaseDaemonClient) ([]map[string]string, error) {
response, err := dbdClient.RunSQLPlusFormatted(ctx, sqlRequest)
if err != nil {
return nil, fmt.Errorf("failed to run query %q: %v", sqlRequest.GetCommands(), err)
}
return parseSQLResponse(response)
}
// parseSQLResponse parses the JSON result-set (returned by runSQLPlus API) and
// returns a list of rows with column-value mapping.
func parseSQLResponse(resp *dbdpb.RunCMDResponse) ([]map[string]string, error) {
var rows []map[string]string
for _, msg := range resp.GetMsg() {
row := make(map[string]string)
if err := json.Unmarshal([]byte(msg), &row); err != nil {
return nil, fmt.Errorf("failed to parse %s: %v", msg, err)
}
rows = append(rows, row)
}
return rows, nil
}