oracle/controllers/config_agent_helpers.go (1,401 lines of code) (raw):
// Copyright 2021 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 controllers
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/controllers/standbyhelpers"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/backup"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/common/sql"
"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/agents/standby"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/database/provision"
"github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/util/secret"
lropb "google.golang.org/genproto/googleapis/longrunning"
"google.golang.org/protobuf/types/known/timestamppb"
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
version = "12.2"
pdbAdmin = "GPDB_ADMIN"
gsmSecretStr = "projects/%s/secrets/%s/versions/%s"
)
var (
newGsmClient = func(ctx context.Context) (*secretmanager.Client, func() error, error) {
client, err := secretmanager.NewClient(ctx)
if err != nil {
return nil, func() error { return nil }, err
}
return client, client.Close, nil
}
)
var overrideParamTypeStatic = map[string]bool{
"sga_target": true,
}
// GetLROOperation returns LRO operation for the specified namespace instance and operation id.
func GetLROOperation(ctx context.Context, dbClientFactory DatabaseClientFactory, r client.Reader, id, namespace, instName string) (*lropb.Operation, error) {
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, err
}
defer closeConn()
req := &lropb.GetOperationRequest{Name: id}
return dbClient.GetOperation(ctx, req)
}
// DeleteLROOperation deletes LRO operation for the specified namespace instance and operation id.
func DeleteLROOperation(ctx context.Context, dbClientFactory DatabaseClientFactory, r client.Reader, id, namespace, instName string) error {
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return err
}
defer closeConn()
_, err = dbClient.DeleteOperation(ctx, &lropb.DeleteOperationRequest{Name: id})
return err
}
// Check for LRO job status
// Return (true, nil) if LRO is done without errors.
// Return (true, err) if LRO is done with an error.
// Return (false, nil) if LRO still in progress.
// Return (false, err) if other error occurred.
func IsLROOperationDone(ctx context.Context, dbClientFactory DatabaseClientFactory, r client.Reader, id, namespace, instName string) (bool, error) {
operation, err := GetLROOperation(ctx, dbClientFactory, r, id, namespace, instName)
if err != nil {
return false, err
}
if !operation.GetDone() {
return false, nil
}
// handle case when remote LRO completed unsuccessfully
if operation.GetError() != nil {
return true, fmt.Errorf("Operation failed with err: %s. %v", operation.GetError().GetMessage(), err)
}
return true, nil
}
type CreateCDBRequest struct {
OracleHome string
Sid string
DbUniqueName string
CharacterSet string
MemoryPercent int32
AdditionalParams []string
Version string
DbDomain string
LroInput *LROInput
}
type LROInput struct {
OperationId string
}
// CreateCDB creates a CDB using dbca.
func CreateCDB(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req CreateCDBRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/CreateCDB", "namespace", namespace, "instName", instName, "sid", req.Sid)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CreateCDB: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
lro, err := dbClient.CreateCDBAsync(ctx, &dbdpb.CreateCDBAsyncRequest{
SyncRequest: &dbdpb.CreateCDBRequest{
OracleHome: req.OracleHome,
DatabaseName: req.Sid,
Version: req.Version,
DbUniqueName: req.DbUniqueName,
CharacterSet: req.CharacterSet,
MemoryPercent: req.MemoryPercent,
AdditionalParams: req.AdditionalParams,
DbDomain: req.DbDomain,
},
LroInput: &dbdpb.LROInput{OperationId: req.LroInput.OperationId},
})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CreateCDB: failed to create CDB: %v", err)
}
klog.InfoS("config_agent_helpers/CreateCDB successfully completed")
return lro, nil
}
type BounceDatabaseRequest struct {
Sid string
// avoid_config_backup: by default we backup the config except for scenarios
// when it isn't possible (like bootstrapping)
AvoidConfigBackup bool
}
// BounceDatabase shutdown/startup the database as requested.
func BounceDatabase(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req BounceDatabaseRequest) error {
klog.InfoS("config_agent_helpers/BounceDatabase", "namespace", namespace, "instName", instName, "sid", req.Sid)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return err
}
defer closeConn()
klog.InfoS("config_agent_helpers/BounceDatabase shutting down database")
_, err = dbClient.BounceDatabase(ctx, &dbdpb.BounceDatabaseRequest{
Operation: dbdpb.BounceDatabaseRequest_SHUTDOWN,
DatabaseName: req.Sid,
Option: "immediate",
})
if err != nil {
return fmt.Errorf("config_agent_helpers/BounceDatabase: error while shutting db: %v", err)
}
klog.InfoS("config_agent_helpers/BounceDatabase: shutdown successful")
_, err = dbClient.BounceDatabase(ctx, &dbdpb.BounceDatabaseRequest{
Operation: dbdpb.BounceDatabaseRequest_STARTUP,
DatabaseName: req.Sid,
AvoidConfigBackup: req.AvoidConfigBackup,
})
if err != nil {
return fmt.Errorf("config_agent_helpers/BounceDatabase: error while starting db: %v", err)
}
klog.InfoS("config_agent_helpers/BounceDatabase: startup successful")
return err
}
func RecoverConfigFile(ctx context.Context, dbClientFactory DatabaseClientFactory, r client.Reader, namespace, instName, cdbName string) error {
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return err
}
defer closeConn()
if _, err := dbClient.RecoverConfigFile(ctx, &dbdpb.RecoverConfigFileRequest{CdbName: cdbName}); err != nil {
klog.InfoS("config_agent_helpers/configagent/RecoverConfigFile: error while recovering config file: err", "err", err)
return fmt.Errorf("config_agent_helpers/RecoverConfigFile: failed to recover config file due to: %v", err)
}
klog.InfoS("config_agent_helpers/configagent/RecoverConfigFile: config file backup successful")
return err
}
type CreateDatabaseRequest struct {
CdbName string
Name string
// only being used for plaintext password scenario.
// GSM doesn't use this field.
Password string
DbDomain string
AdminPasswordGsmSecretRef *GsmSecretReference
// only being used for plaintext password scenario.
// GSM doesn't use this field.
LastPassword string
}
type CreateDatabaseResponse struct {
Status string
ErrorMessage string
}
// CreateDatabase creates PDB as requested.
func CreateDatabase(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req CreateDatabaseRequest) (string, error) {
klog.InfoS("config_agent_helpers/CreateDatabase", "namespace", namespace, "instName", instName, "cdbName", req.CdbName, "pdbName", req.Name)
var pwd string
var err error
toUpdatePlaintextAdminPwd := req.Password != "" && req.Password != req.LastPassword
if toUpdatePlaintextAdminPwd {
pwd = req.Password
}
toUpdateGsmAdminPwd := req.AdminPasswordGsmSecretRef != nil && (req.AdminPasswordGsmSecretRef.Version != req.AdminPasswordGsmSecretRef.LastVersion || req.AdminPasswordGsmSecretRef.Version == "latest")
if toUpdateGsmAdminPwd {
pwd, err = AccessSecretVersionFunc(ctx, fmt.Sprintf(gsmSecretStr, req.AdminPasswordGsmSecretRef.ProjectId, req.AdminPasswordGsmSecretRef.SecretId, req.AdminPasswordGsmSecretRef.Version))
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateDatabase: failed to retrieve secret from Google Secret Manager: %v", err)
}
}
p, err := buildPDB(req.CdbName, req.Name, pwd, version, consts.ListenerNames, true)
if err != nil {
return "", err
}
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateDatabase: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
klog.InfoS("config_agent_helpers/CreateDatabase: checking CDB state")
_, err = dbClient.CheckDatabaseState(ctx, &dbdpb.CheckDatabaseStateRequest{IsCdb: true, DatabaseName: req.CdbName, DbDomain: req.DbDomain})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateDatabase: failed to check a CDB state: %v", err)
}
klog.InfoS("config_agent_helpers/CreateDatabase: pre-flight check#1: CDB is up and running")
pdbCheckCmd := []string{fmt.Sprintf("select open_mode, restricted from v$pdbs where name = '%s'", sql.StringParam(p.pluggableDatabaseName))}
resp, err := dbClient.RunSQLPlusFormatted(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: pdbCheckCmd, Suppress: false})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateDatabase: failed to check if a PDB called %s already exists: %v", p.pluggableDatabaseName, err)
}
klog.InfoS("config_agent_helpers/CreateDatabase pre-flight check#2", "pdb", p.pluggableDatabaseName, "resp", resp)
if resp != nil && resp.Msg != nil {
if toUpdateGsmAdminPwd || toUpdatePlaintextAdminPwd {
sqls := append([]string{sql.QuerySetSessionContainer(p.pluggableDatabaseName)}, []string{sql.QueryAlterUser(pdbAdmin, pwd)}...)
if _, err := dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{
Commands: sqls,
Suppress: true,
}); err != nil {
return "", fmt.Errorf("failed to alter user %s: %v", pdbAdmin, err)
}
klog.InfoS("config_agent_helpers/CreateDatabase update pdb admin user succeeded", "user", pdbAdmin)
return "AdminUserSyncCompleted", nil
}
klog.InfoS("config_agent_helpers/CreateDatabase pre-flight check#2", "pdb", p.pluggableDatabaseName, "respMsg", resp.Msg)
return "AlreadyExists", nil
}
klog.InfoS("config_agent_helpers/CreateDatabase pre-flight check#2: pdb doesn't exist, proceeding to create", "pdb", p.pluggableDatabaseName)
cdbDir := fmt.Sprintf(consts.DataDir, consts.DataMount, req.CdbName)
pdbDir := filepath.Join(cdbDir, strings.ToUpper(req.Name))
toCreate := []string{
fmt.Sprintf("%s/data", pdbDir),
fmt.Sprintf("%s/%s", pdbDir, consts.DpdumpDir.Linux),
fmt.Sprintf("%s/rman", consts.OracleBase),
}
var dirs []*dbdpb.CreateDirsRequest_DirInfo
for _, d := range toCreate {
dirs = append(dirs, &dbdpb.CreateDirsRequest_DirInfo{
Path: d,
Perm: 0760,
})
}
if _, err := dbClient.CreateDirs(ctx, &dbdpb.CreateDirsRequest{Dirs: dirs}); err != nil {
return "", fmt.Errorf("failed to create PDB dirs: %v", err)
}
pdbCmd := []string{sql.QueryCreatePDB(p.pluggableDatabaseName, pdbAdmin, p.pluggableAdminPasswd, p.dataFilesDir, p.defaultTablespace, p.defaultTablespaceDatafile, p.fileConvertFrom, p.fileConvertTo)}
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: pdbCmd, Suppress: false})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateDatabase: failed to create a PDB %s: %v", p.pluggableDatabaseName, err)
}
klog.InfoS("config_agent_helpers/CreateDatabase create a PDB Done", "pdb", p.pluggableDatabaseName)
pdbOpen := []string{
fmt.Sprintf("alter pluggable database %s open read write", sql.MustBeObjectName(p.pluggableDatabaseName)),
fmt.Sprintf("alter pluggable database %s save state", sql.MustBeObjectName(p.pluggableDatabaseName)),
}
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: pdbOpen, Suppress: false})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreatePDBDatabase: PDB %s open failed: %v", p.pluggableDatabaseName, err)
}
klog.InfoS("config_agent_helpers/CreateDatabase PDB open", "pdb", p.pluggableDatabaseName)
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: []string{
sql.QuerySetSessionContainer(p.pluggableDatabaseName),
sql.QueryGrantPrivileges("create session, dba", pdbAdmin),
sql.QueryGrantPrivileges("create session, resource, datapump_imp_full_database, datapump_exp_full_database, unlimited tablespace", consts.PDBLoaderUser),
}, Suppress: false})
if err != nil {
// Until we have a proper error handling, just log an error here.
klog.ErrorS(err, "CreateDatabase: failed to create a PDB_ADMIN user and/or PDB loader user")
}
klog.InfoS("config_agent_helpers/CreateDatabase: created PDB_ADMIN and PDB Loader users")
// Separate out the directory treatment for the ease of troubleshooting.
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: []string{
sql.QuerySetSessionContainer(p.pluggableDatabaseName),
sql.QueryCreateDir(consts.DpdumpDir.Oracle, filepath.Join(p.pathPrefix, consts.DpdumpDir.Linux)),
sql.QueryGrantPrivileges(fmt.Sprintf("read,write on directory %s", consts.DpdumpDir.Oracle), consts.PDBLoaderUser),
}, Suppress: false})
if err != nil {
klog.ErrorS(err, "CreateDatabase: failed to create a Data Pump directory", "datapumpDir", consts.DpdumpDir)
}
klog.InfoS("config_agent_helpers/CreateDatabase: DONE", "pdb", p.pluggableDatabaseName)
return "Ready", nil
}
type DeleteDatabaseRequest struct {
Name string
DbDomain string
}
// DeleteDatabase deletes the specified Database(PDB)
func DeleteDatabase(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req DeleteDatabaseRequest) error {
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/CreateDatabase: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
pdbName := strings.ToUpper(req.Name)
pdbCheckCmd := []string{fmt.Sprintf("select open_mode, restricted from v$pdbs where name = '%s'", sql.StringParam(pdbName))}
resp, err := dbClient.RunSQLPlusFormatted(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: pdbCheckCmd, Suppress: false})
if err != nil {
return fmt.Errorf("config_agent_helpers/DeleteDatabase: failed to check if a PDB named %s already exists: %v", pdbName, err)
}
if resp != nil && resp.Msg != nil {
klog.InfoS("config_agent_helpers/DeleteDatabase completed pre-flight check. The PDB exists.", "pdb", pdbName, "resp", resp)
} else {
klog.InfoS(fmt.Sprintf("config_agent_helpers/DeleteDatabase: A PDB named %s was not found", pdbName))
return nil
}
closePdbCmd := []string{fmt.Sprintf("alter pluggable database %s close immediate", sql.StringParam(pdbName))}
_, err = dbClient.RunSQLPlusFormatted(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: closePdbCmd, Suppress: false})
if err != nil {
return fmt.Errorf("config_agent_helpers/DeleteDatabase: failed to close the PDB named %s: %v", pdbName, err)
}
deletePdbCmd := []string{fmt.Sprintf("drop pluggable database %s including datafiles", sql.StringParam(pdbName))}
_, err = dbClient.RunSQLPlusFormatted(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: deletePdbCmd, Suppress: false})
if err != nil {
return fmt.Errorf("config_agent_helpers/DeleteDatabase: failed to delete the PDB named %s: %v", pdbName, err)
}
return nil
}
type UsersChangedRequest struct {
PdbName string
UserSpecs []*User
}
type UsersChangedResponse struct {
Changed bool
Suppressed []*UsersChangedResponseSuppressed
}
type UsersChangedResponseSuppressed struct {
SuppressType UsersChangedResponseType
UserName string
// sql is the suppressed cmd which can update the user to the spec defined
// state
Sql string
}
type UsersChangedResponseType int32
const (
UsersChangedResponse_UNKNOWN_TYPE UsersChangedResponseType = 0
UsersChangedResponse_DELETE UsersChangedResponseType = 1
UsersChangedResponse_CREATE UsersChangedResponseType = 2
)
// UsersChanged determines whether there is change on users (update/delete/create).
func UsersChanged(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req UsersChangedRequest) (*UsersChangedResponse, error) {
klog.InfoS("config_agent_helpers/UsersChanged", "namespace", namespace, "instName", instName, "pdbName", req.PdbName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, err
}
defer closeConn()
us := newUsers(req.PdbName, req.UserSpecs)
toCreate, toUpdate, toDelete, toUpdatePwd, err := us.diff(ctx, dbClient)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/UsersChanged: failed to get difference between env and spec for users: %v", err)
}
var suppressed []*UsersChangedResponseSuppressed
for _, du := range toDelete {
suppressed = append(suppressed, &UsersChangedResponseSuppressed{
SuppressType: UsersChangedResponse_DELETE,
UserName: du.userName,
Sql: du.delete(),
})
}
for _, cu := range toCreate {
if cu.newPassword == "" {
suppressed = append(suppressed, &UsersChangedResponseSuppressed{
SuppressType: UsersChangedResponse_CREATE,
UserName: cu.userName,
})
}
}
resp := &UsersChangedResponse{
Changed: len(toCreate) != 0 || len(toUpdate) != 0 || len(toUpdatePwd) != 0,
Suppressed: suppressed,
}
klog.InfoS("config_agent_helpers/UsersChanged: DONE", "resp", resp)
return resp, nil
}
type UpdateUsersRequest struct {
PdbName string
UserSpecs []*User
}
// UpdateUsers update/create users as requested.
func UpdateUsers(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req UpdateUsersRequest) error {
klog.InfoS("config_agent_helpers/UpdateUsers", "namespace", namespace, "instName", instName, "pdbName", req.PdbName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/UpdateUsers: failed to create database daemon client: %v", err)
}
defer closeConn()
us := newUsers(req.PdbName, req.UserSpecs)
toCreate, toUpdate, _, toUpdatePwd, err := us.diff(ctx, dbClient)
if err != nil {
return fmt.Errorf("config_agent_helpers/UpdateUsers: failed to get difference between env and spec for users: %v", err)
}
foundErr := false
for _, u := range toCreate {
klog.InfoS("config_agent_helpers/UpdateUsers", "creating user", u.userName)
if err := u.create(ctx, dbClient); err != nil {
klog.ErrorS(err, "failed to create user")
foundErr = true
}
}
for _, u := range toUpdate {
klog.InfoS("config_agent_helpers/UpdateUsers", "updating user", u.userName)
// we found there is a scenario that role comes with privileges. For example
// Grant dba role to a user will automatically give unlimited tablespace privilege.
// Revoke dba role will automatically revoke unlimited tablespace privilege.
// thus user update will first update role and then update sys privi.
if err := u.update(ctx, dbClient, us.databaseRoles); err != nil {
klog.ErrorS(err, "failed to update user")
foundErr = true
}
}
for _, u := range toUpdatePwd {
klog.InfoS("config_agent_helpers/UpdateUsers", "updating user pwd", u.userName)
if err := u.updatePassword(ctx, dbClient); err != nil {
klog.ErrorS(err, "failed to update user password")
foundErr = true
}
}
if foundErr {
return errors.New("failed to update users")
}
klog.InfoS("config_agent_helpers/UpdateUsers: DONE")
return nil
}
// SetParameter sets database parameter as requested.
func SetParameter(ctx context.Context, dbClientFactory DatabaseClientFactory, r client.Reader, namespace, instName, key, value string) (bool, error) {
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return false, err
}
defer closeConn()
// Fetch parameter type
// The possible values are IMMEDIATE FALSE DEFERRED
query := fmt.Sprintf("select issys_modifiable from v$parameter where name='%s'", sql.StringParam(key))
paramType, err := fetchAndParseSingleResultQuery(ctx, dbClient, query)
if err != nil {
return false, fmt.Errorf("config_agent_helpers/SetParameter: error while inferring parameter type: %v", err)
}
query = fmt.Sprintf("select type from v$parameter where name='%s'", sql.StringParam(key))
paramDatatype, err := fetchAndParseSingleResultQuery(ctx, dbClient, query)
if err != nil {
return false, fmt.Errorf("config_agent_helpers/SetParameter: error while inferring parameter data type: %v", err)
}
// string parameters need to be quoted,
// those have type 2, see the link for the parameter types description
// https://docs.oracle.com/database/121/REFRN/GUID-C86F3AB0-1191-447F-8EDF-4727D8693754.htm
isStringParam := paramDatatype == "2"
command, err := sql.QuerySetSystemParameterNoPanic(key, value, isStringParam)
if err != nil {
return false, fmt.Errorf("config_agent_helpers/SetParameter: error constructing set parameter query: %v", err)
}
isStatic := false
if paramType == "FALSE" || overrideParamTypeStatic[key] {
klog.InfoS("config_agent_helpers/SetParameter", "parameter_type", "STATIC")
command = fmt.Sprintf("%s scope=spfile", command)
isStatic = true
}
if paramType == "DEFERRED" {
command = fmt.Sprintf("%s deferred", command)
}
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{
Commands: []string{command},
Suppress: false,
})
if err != nil {
return false, fmt.Errorf("config_agent_helpers/SetParameter: error while executing parameter command: %q", command)
}
return isStatic, nil
}
// fetchAndParseSingleResultQuery is a utility method intended for running single result queries.
// It parses the single column JSON result-set (returned by runSQLPlus API) and returns a list.
func fetchAndParseSingleResultQuery(ctx context.Context, client dbdpb.DatabaseDaemonClient, query string) (string, error) {
sqlRequest := &dbdpb.RunSQLPlusCMDRequest{
Commands: []string{query},
Suppress: false,
}
response, err := client.RunSQLPlusFormatted(ctx, sqlRequest)
if err != nil {
return "", fmt.Errorf("failed to run query %q; DSN: %q; error: %v", query, sqlRequest.GetDsn(), err)
}
if response == nil {
return "", nil
}
result, err := parseSQLResponse(response)
if err != nil {
return "", fmt.Errorf("error while parsing query response: %q; error: %v", query, err)
}
var rows []string
for _, row := range result {
if len(row) != 1 {
return "", fmt.Errorf("config_agent_helpers/fetchAndParseSingleColumnMultiRowQueriesFromEM: # of cols returned by query != 1: %v", row)
}
for _, v := range row {
rows = append(rows, v)
}
}
if len(rows) < 1 {
return "", nil
}
return rows[0], nil
}
// 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
}
type CreateUsersRequest struct {
CdbName string
PdbName string
CreateUsersCmd []string
GrantPrivsCmd []string
DbDomain string
User []*User
}
type User struct {
Name string
// only being used for plaintext password scenario.
// GSM doesn't use this field.
Password string
Privileges []string
PasswordGsmSecretRef *GsmSecretReference
// only being used for plaintext password scenario.
// GSM doesn't use this field.
LastPassword string
}
type GsmSecretReference struct {
ProjectId string
SecretId string
Version string
LastVersion string
}
// pdb represents a PDB database.
type pdb struct {
containerDatabaseName string
dataFilesDir string
defaultTablespace string
defaultTablespaceDatafile string
fileConvertFrom string
fileConvertTo string
hostName string
listenerDir string
listeners map[string]*consts.Listener
pathPrefix string
pluggableAdminPasswd string
pluggableDatabaseName string
skipUserCheck bool
version string
}
func buildPDB(cdbName, pdbName, pdbAdminPass, version string, listeners map[string]*consts.Listener, skipUserCheck bool) (*pdb, error) {
// For consistency sake, keeping all PDB names uppercase.
pdbName = strings.ToUpper(pdbName)
host, err := os.Hostname()
if err != nil {
return nil, err
}
return &pdb{
pluggableDatabaseName: pdbName,
pluggableAdminPasswd: pdbAdminPass,
containerDatabaseName: cdbName,
dataFilesDir: fmt.Sprintf(consts.PDBDataDir, consts.DataMount, cdbName, pdbName),
defaultTablespace: fmt.Sprintf("%s_USERS", pdbName),
defaultTablespaceDatafile: fmt.Sprintf(consts.PDBDataDir+"/%s_users.dbf", consts.DataMount, cdbName, pdbName, strings.ToLower(pdbName)),
pathPrefix: fmt.Sprintf(consts.PDBPathPrefix, consts.DataMount, cdbName, pdbName),
fileConvertFrom: fmt.Sprintf(consts.PDBSeedDir, consts.DataMount, cdbName),
fileConvertTo: fmt.Sprintf(consts.PDBDataDir, consts.DataMount, cdbName, pdbName),
listenerDir: fmt.Sprintf(consts.ListenerDir, consts.DataMount),
listeners: listeners,
version: version,
hostName: host,
skipUserCheck: skipUserCheck,
}, nil
}
// CreateUsers creates users as requested.
func CreateUsers(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req CreateUsersRequest) (string, error) {
// UsersChanged is called before this function by caller (db controller) to check if
// the users requested are already existing.
// Thus no duplicated list user check is performed here.
klog.InfoS("config_agent_helpers/CreateUsers", "namespace", namespace, "cdbName", req.CdbName, "pdbName", req.PdbName)
p, err := buildPDB(req.CdbName, req.PdbName, "", version, consts.ListenerNames, true)
if err != nil {
return "", err
}
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: failed to create database daemon client: %v", err)
}
defer closeConn()
klog.InfoS("config_agent_helpers/CreateUsers: checking CDB state")
_, err = dbClient.CheckDatabaseState(ctx, &dbdpb.CheckDatabaseStateRequest{IsCdb: true, DatabaseName: req.CdbName, DbDomain: req.DbDomain})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: failed to check a CDB state: %v", err)
}
klog.InfoS("config_agent_helpers/CreateUsers: pre-flight check#: CDB is up and running")
// Separate create users from grants to make troubleshooting easier.
usersCmd := []string{sql.QuerySetSessionContainer(p.pluggableDatabaseName)}
usersCmd = append(usersCmd, req.CreateUsersCmd...)
for _, u := range req.User {
if u.PasswordGsmSecretRef != nil && u.Name != "" {
var pwd string
pwd, err = AccessSecretVersionFunc(ctx, fmt.Sprintf(gsmSecretStr, u.PasswordGsmSecretRef.ProjectId, u.PasswordGsmSecretRef.SecretId, u.PasswordGsmSecretRef.Version))
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: failed to retrieve secret from Google Secret Manager: %v", err)
}
if _, err = sql.Identifier(pwd); err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: Google Secret Manager contains an invalid password for user %q: %v", u.Name, err)
}
usersCmd = append(usersCmd, sql.QueryCreateUser(u.Name, pwd))
}
}
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: usersCmd, Suppress: true})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: failed to create users in a PDB %s: %v", p.pluggableDatabaseName, err)
}
klog.InfoS("config_agent_helpers/CreateUsers: create users in PDB DONE", "pdb", p.pluggableDatabaseName)
privsCmd := []string{sql.QuerySetSessionContainer(p.pluggableDatabaseName)}
privsCmd = append(privsCmd, req.GrantPrivsCmd...)
_, err = dbClient.RunSQLPlus(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: privsCmd, Suppress: false})
if err != nil {
return "", fmt.Errorf("config_agent_helpers/CreateUsers: failed to grant privileges in a PDB %s: %v", p.pluggableDatabaseName, err)
}
klog.InfoS("config_agent_helpers/CreateUsers: DONE", "pdb", p.pluggableDatabaseName)
return "Ready", nil
}
// AccessSecretVersionFunc accesses the payload for the given secret version if one
// exists. The version can be a version number as a string (e.g. "5") or an
// alias (e.g. "latest").
var AccessSecretVersionFunc = func(ctx context.Context, name string) (string, error) {
// Create the GSM client.
client, closeConn, err := newGsmClient(ctx)
if err != nil {
return "", fmt.Errorf("config_agent_helpers/AccessSecretVersionFunc: failed to create secretmanager client: %v", err)
}
defer closeConn()
// Build the request.
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: name,
}
// Call the API.
result, err := client.AccessSecretVersion(ctx, req)
if err != nil {
return "", fmt.Errorf("config_agent_helpers/AccessSecretVersionFunc: failed to access secret version: %v", err)
}
return string(result.Payload.Data[:]), nil
}
type BootstrapDatabaseRequest struct {
CdbName string
Version string
Host string
DbUniqueName string
Dbdomain string
Mode BootstrapDatabaseRequestBootstrapMode
LroInput *LROInput
}
type BootstrapDatabaseRequestBootstrapMode int32
const (
BootstrapDatabaseRequest_ProvisionUnseeded BootstrapDatabaseRequestBootstrapMode = 0
BootstrapDatabaseRequest_ProvisionSeeded BootstrapDatabaseRequestBootstrapMode = 1
BootstrapDatabaseRequest_Restore BootstrapDatabaseRequestBootstrapMode = 2
)
// BootstrapDatabase bootstrap a CDB after creation or restore.
func BootstrapDatabase(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req BootstrapDatabaseRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/BootstrapDatabase", "namespace", namespace, "instName", instName, "cdbName", req.CdbName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: failed to create database daemon client: %v", err)
}
defer closeConn()
resp, err := dbClient.FileExists(ctx, &dbdpb.FileExistsRequest{Name: consts.ProvisioningDoneFile})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: failed to check a provisioning file: %v", err)
}
if resp.Exists {
klog.InfoS("config_agent_helpers/BootstrapDatabase: provisioning file found, skip bootstrapping")
return &lropb.Operation{Done: true}, nil
}
switch req.Mode {
case BootstrapDatabaseRequest_ProvisionUnseeded:
task := provision.NewBootstrapDatabaseTaskForUnseeded(req.CdbName, req.DbUniqueName, req.Dbdomain, dbClient)
if err := task.Call(ctx); err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: failed to bootstrap database : %v", err)
}
case BootstrapDatabaseRequest_ProvisionSeeded:
lro, err := dbClient.BootstrapDatabaseAsync(ctx, &dbdpb.BootstrapDatabaseAsyncRequest{
SyncRequest: &dbdpb.BootstrapDatabaseRequest{
CdbName: req.CdbName,
DbDomain: req.Dbdomain,
},
LroInput: &dbdpb.LROInput{OperationId: req.LroInput.OperationId},
})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: error while call dbdaemon/BootstrapDatabase: %v", err)
}
return lro, nil
default:
}
if _, err = dbClient.CreateListener(ctx, &dbdpb.CreateListenerRequest{
DatabaseName: req.CdbName,
Port: consts.SecureListenerPort,
Protocol: "TCP",
DbDomain: req.Dbdomain,
}); err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: error while creating listener: %v", err)
}
if _, err = dbClient.CreateFile(ctx, &dbdpb.CreateFileRequest{
Path: consts.ProvisioningDoneFile,
}); err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapDatabase: error while creating provisioning done file: %v", err)
}
return &lropb.Operation{Done: true}, nil
}
type BootstrapStandbyRequest struct {
CdbName string
Version string
Dbdomain string
}
type BootstrapStandbyResponseUser struct {
UserName string
Privs []string
}
type BootstrapStandbyResponsePDB struct {
PdbName string
Users []*BootstrapStandbyResponseUser
}
// BootstrapStandby performs bootstrap steps for standby instance.
func BootstrapStandby(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req BootstrapStandbyRequest) ([]*BootstrapStandbyResponsePDB, error) {
klog.InfoS("config_agent_helpers/BootstrapStandby", "namespace", namespace, "instName", instName, "cdbName", req.CdbName, "version", req.Version, "dbdomain", req.Dbdomain)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapStandby: failed to create database daemon client: %v", err)
}
defer closeConn()
klog.InfoS("BootstrapStandby running")
if err := standby.BootstrapStandby(ctx, dbClient); err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapStandby: failed to bootstrap standby database : %v", err)
}
klog.InfoS("config_agent_helpers/BootstrapStandby: bootstrap task completed successfully")
// fetch existing pdbs/users to create database resources for
knownPDBsResp, err := dbClient.KnownPDBs(ctx, &dbdpb.KnownPDBsRequest{
IncludeSeed: false,
OnlyOpen: false,
})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapStandby: dbdaemon failed to get KnownPDBs: %v", err)
}
var migratedPDBs []*BootstrapStandbyResponsePDB
for _, pdb := range knownPDBsResp.GetKnownPdbs() {
us := newUsers(pdb, []*User{})
_, _, existingUsers, _, err := us.diff(ctx, dbClient)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/BootstrapStandby: failed to get existing users for pdb %v: %v", pdb, err)
}
var migratedUsers []*BootstrapStandbyResponseUser
for _, u := range existingUsers {
migratedUsers = append(migratedUsers, &BootstrapStandbyResponseUser{
UserName: u.GetUserName(),
Privs: u.GetUserEnvPrivs(),
})
}
migratedPDBs = append(migratedPDBs, &BootstrapStandbyResponsePDB{
PdbName: strings.ToLower(pdb),
Users: migratedUsers,
})
}
klog.InfoS("config_agent_helpers/BootstrapStandby: fetch existing pdbs and users successfully.", "MigratedPDBs", migratedPDBs)
return migratedPDBs, nil
}
type CreateListenerRequest struct {
Name string
Port int32
Protocol string
OracleHome string
DbDomain string
}
// CreateListener invokes dbdaemon.CreateListener.
func CreateListener(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req *CreateListenerRequest) error {
klog.InfoS("config_agent_helpers/CreateListener", "namespace", namespace, "instName", instName, "listenerName", req.Name, "port", req.Port, "protocol", req.Protocol, "oracleHome", req.OracleHome, "dbDomain", req.DbDomain)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/CreateListener: failed to create listener: %v", err)
}
defer closeConn()
klog.InfoS("config_agent_helpers/CreateListener: creating listener")
_, err = dbClient.CreateListener(ctx, &dbdpb.CreateListenerRequest{
DatabaseName: req.Name,
Port: req.Port,
Protocol: req.Protocol,
OracleHome: req.OracleHome,
DbDomain: req.DbDomain,
})
if err != nil {
return fmt.Errorf("config_agent_helpers/CreateListener: error while creating listener: %v", err)
}
return nil
}
type VerifyPhysicalBackupRequest struct {
GcsPath string
}
type VerifyPhysicalBackupResponse struct {
ErrMsgs []string
}
// VerifyPhysicalBackup verifies the existence of physical backup.
func VerifyPhysicalBackup(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req VerifyPhysicalBackupRequest) (*VerifyPhysicalBackupResponse, error) {
klog.InfoS("config_agent_helpers/VerifyPhysicalBackup", "namespace", namespace, "instName", instName, "gcsPath", req.GcsPath)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/VerifyPhysicalBackup: failed to create a database daemon dbdClient: %v", err)
}
defer closeConn()
if _, err := dbClient.DownloadDirectoryFromGCS(ctx, &dbdpb.DownloadDirectoryFromGCSRequest{
GcsPath: req.GcsPath,
AccessPermissionCheck: true,
}); err != nil {
return &VerifyPhysicalBackupResponse{ErrMsgs: []string{err.Error()}}, nil
}
return &VerifyPhysicalBackupResponse{}, nil
}
type PhysicalBackupRequest struct {
BackupSubType PhysicalBackupRequest_Type
BackupItems []string
Backupset bool
Compressed bool
CheckLogical bool
// DOP = degree of parallelism for physical backup.
Dop int32
Level int32
Filesperset int32
SectionSize int32
LocalPath string
GcsPath string
LroInput *LROInput
BackupTag string
}
type PhysicalBackupRequest_Type int32
const (
PhysicalBackupRequest_UNKNOWN_TYPE PhysicalBackupRequest_Type = 0
PhysicalBackupRequest_INSTANCE PhysicalBackupRequest_Type = 1
PhysicalBackupRequest_DATABASE PhysicalBackupRequest_Type = 2
PhysicalBackupRequest_TABLESPACE PhysicalBackupRequest_Type = 3
PhysicalBackupRequest_DATAFILE PhysicalBackupRequest_Type = 4
)
// PhysicalBackup starts an RMAN backup and stores it in the GCS bucket provided.
func PhysicalBackup(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req PhysicalBackupRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/PhysicalBackup", "namespace", namespace, "instName", instName, "gcsPath", req.GcsPath, "localPath", req.LocalPath)
var granularity string
switch req.BackupSubType {
case PhysicalBackupRequest_INSTANCE:
granularity = "database"
case PhysicalBackupRequest_DATABASE:
if req.BackupItems == nil {
return &lropb.Operation{}, fmt.Errorf("config_agent_helpers/PhysicalBackup: failed a pre-flight check: a PDB backup is requested, but no PDB name(s) given")
}
granularity = "pluggable database "
for i, pdb := range req.BackupItems {
if i == 0 {
granularity += pdb
} else {
granularity += ", "
granularity += pdb
}
}
default:
return &lropb.Operation{}, fmt.Errorf("config_agent_helpers/PhysicalBackup: unsupported in this release sub backup type of %v", req.BackupSubType)
}
klog.InfoS("config_agent_helpers/PhysicalBackup", "granularity", granularity)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackup: failed to create database daemon client: %v", err)
}
defer closeConn()
klog.InfoS("config_agent_helpers/PhysicalBackup: creating physical backup")
sectionSize := resource.NewQuantity(int64(req.SectionSize), resource.DecimalSI)
return backup.PhysicalBackup(ctx, &backup.Params{
Client: dbClient,
Granularity: granularity,
Backupset: req.Backupset,
CheckLogical: req.CheckLogical,
Compressed: req.Compressed,
DOP: req.Dop,
Level: req.Level,
Filesperset: req.Filesperset,
SectionSize: *sectionSize,
LocalPath: req.LocalPath,
GCSPath: req.GcsPath,
BackupTag: req.BackupTag,
OperationID: req.LroInput.OperationId,
})
}
type PhysicalRestoreRequest struct {
InstanceName string
CdbName string
// DOP = degree of parallelism for a restore from a physical backup.
Dop int32
LocalPath string
GcsPath string
LroInput *LROInput
LogGcsPath string
Incarnation string
BackupIncarnation string
StartTime *timestamppb.Timestamp
EndTime *timestamppb.Timestamp
StartScn int64
EndScn int64
}
// PhysicalRestore restores an RMAN backup (downloaded from GCS).
func PhysicalRestore(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req PhysicalRestoreRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/PhysicalRestore", "namespace", namespace, "instName", instName, "gcsPath", req.GcsPath, "localPath", req.LocalPath)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalRestore: failed to create database daemon client: %v", err)
}
defer closeConn()
return backup.PhysicalRestore(ctx, &backup.Params{
Client: dbClient,
InstanceName: req.InstanceName,
CDBName: req.CdbName,
DOP: req.Dop,
LocalPath: req.LocalPath,
GCSPath: req.GcsPath,
OperationID: req.LroInput.OperationId,
LogGcsDir: req.LogGcsPath,
Incarnation: req.Incarnation,
BackupIncarnation: req.BackupIncarnation,
StartTime: req.StartTime,
EndTime: req.EndTime,
StartSCN: req.StartScn,
EndSCN: req.EndScn,
})
}
type CheckStatusRequest struct {
Name string
CdbName string
CheckStatusType CheckStatusRequest_Type
DbDomain string
}
type CheckStatusRequest_Type int32
const (
CheckStatusRequest_UNKNOWN_TYPE CheckStatusRequest_Type = 0
CheckStatusRequest_INSTANCE CheckStatusRequest_Type = 1
)
type CheckStatusResponse struct {
Status string
ErrorMessage string
}
// CheckStatus runs a requested set of state checks.
// The Instance state check consists of:
// - checking the provisioning done file.
// - running a CDB connection test via DB Daemon.
func CheckStatus(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req CheckStatusRequest) (*CheckStatusResponse, error) {
klog.InfoS("config_agent_helpers/CheckStatus", "namespace", namespace, "instName", instName, "name", req.Name, "cdbName", req.CdbName, "checkStatusType", req.CheckStatusType)
switch req.CheckStatusType {
case CheckStatusRequest_INSTANCE:
klog.InfoS("config_agent_helpers/CheckStatus: running a Database Instance status check...")
default:
return &CheckStatusResponse{}, fmt.Errorf("config_agent_helpers/CheckStatus: unsupported in this release check status type of %v", req.CheckStatusType)
}
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CheckStatus: failed to create database daemon client: %v", err)
}
defer closeConn()
klog.V(1).InfoS("config_agent_helpers/CheckStatus: checking if provisioning file exists")
resp, err := dbClient.FileExists(ctx, &dbdpb.FileExistsRequest{Name: consts.ProvisioningDoneFile})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CheckStatus: failed to check a provisioning file: %v", err)
}
if !resp.Exists {
klog.InfoS("config_agent_helpers/CheckStatus: provisioning file NOT found")
return &CheckStatusResponse{Status: "InProgress"}, nil
}
klog.InfoS("config_agent_helpers/CheckStatus: provisioning file found")
if _, err = dbClient.CheckDatabaseState(ctx, &dbdpb.CheckDatabaseStateRequest{IsCdb: true, DatabaseName: req.CdbName, DbDomain: req.DbDomain}); err != nil {
return nil, fmt.Errorf("config_agent_helpers/CheckStatus: failed to check a Database Instance state: %v", err)
}
klog.InfoS("config_agent_helpers/CheckStatus: Database Instance is up and running")
pdbCheckCmd := []string{"select open_mode, restricted from v$pdbs"}
resp2, err := dbClient.RunSQLPlusFormatted(ctx, &dbdpb.RunSQLPlusCMDRequest{Commands: pdbCheckCmd, Suppress: false})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CheckStatus: failed to get a list of available PDBs: %v", err)
}
klog.InfoS("config_agent_helpers/CheckStatus", "PDB query response", resp2)
return &CheckStatusResponse{Status: "Ready"}, nil
}
type DataPumpImportRequest struct {
PdbName string
DbDomain string
// GCS path to input dump file
GcsPath string
// GCS path to output log file
GcsLogPath string
// Additional command options from the user.
Options map[string]string
LroInput *LROInput
}
var AllowedImpdpParams = map[string]bool{
"TABLE_EXISTS_ACTION": true,
"REMAP_TABLE": true,
"REMAP_SCHEMA": true,
"REMAP_TABLESPACE": true,
"REMAP_DATAFILE": true,
"PARALLEL": true,
"NETWORK_LINK": true,
}
// DataPumpImport imports data dump file provided in GCS path.
func DataPumpImport(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req DataPumpImportRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/DataPumpImport", "namespace", namespace, "instName", instName, "pdbName", req.PdbName, "dbDomain", req.DbDomain, "gcsPath", req.GcsPath)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/DataPumpImport: failed to create database daemon client: %v", err)
}
defer func() { _ = closeConn() }()
commandParams := []string{
"FULL=YES",
"METRICS=YES",
"LOGTIME=ALL",
}
for k, v := range req.Options {
k = strings.ToUpper(k)
if _, found := AllowedImpdpParams[k]; found {
param := k + "=" + v
commandParams = append(commandParams, param)
}
}
return dbClient.DataPumpImportAsync(ctx, &dbdpb.DataPumpImportAsyncRequest{
SyncRequest: &dbdpb.DataPumpImportRequest{
PdbName: req.PdbName,
DbDomain: req.DbDomain,
GcsPath: req.GcsPath,
GcsLogPath: req.GcsLogPath,
CommandParams: commandParams,
},
LroInput: &dbdpb.LROInput{
OperationId: req.LroInput.OperationId,
},
})
}
type DataPumpExportRequest struct {
PdbName string
DbDomain string
ObjectType string
Objects string
GcsPath string
GcsLogPath string
LroInput *LROInput
FlashbackTime string
}
// DataPumpExport exports data pump file to GCS path provided.
func DataPumpExport(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req DataPumpExportRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/DataPumpExport", "namespace", namespace, "instName", instName, "pdbName", req.PdbName, "dbDomain", req.DbDomain, "objects", req.Objects, "gcsPath", req.GcsPath)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/DataPumpExport: failed to create database daemon client: %v", err)
}
defer func() { _ = closeConn() }()
return dbClient.DataPumpExportAsync(ctx, &dbdpb.DataPumpExportAsyncRequest{
SyncRequest: &dbdpb.DataPumpExportRequest{
PdbName: req.PdbName,
DbDomain: req.DbDomain,
ObjectType: req.ObjectType,
Objects: req.Objects,
GcsPath: req.GcsPath,
GcsLogPath: req.GcsLogPath,
FlashbackTime: req.FlashbackTime,
CommandParams: []string{
"METRICS=YES",
"LOGTIME=ALL",
},
},
LroInput: &dbdpb.LROInput{
OperationId: req.LroInput.OperationId,
},
})
}
type GetParameterTypeValueRequest struct {
Keys []string
}
type GetParameterTypeValueResponse struct {
Types []string
Values []string
}
// GetParameterTypeValue returns parameters' type and value by querying DB.
func GetParameterTypeValue(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req GetParameterTypeValueRequest) (*GetParameterTypeValueResponse, error) {
klog.InfoS("config_agent_helpers/GetParameterTypeValue", "namespace", namespace, "instName", instName, "keys", req.Keys)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/GetParameterTypeValue: failed to create database daemon client: %v", err)
}
defer closeConn()
types := []string{}
values := []string{}
for _, key := range req.Keys {
query := fmt.Sprintf("select issys_modifiable from v$parameter where name='%s'", sql.StringParam(key))
value, err := fetchAndParseSingleResultQuery(ctx, dbClient, query)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/GetParameterTypeValue: error while fetching type for %v: %v", key, err)
}
types = append(types, value)
}
for _, key := range req.Keys {
query := fmt.Sprintf("select value from v$parameter where name='%s'", sql.StringParam(key))
value, err := fetchAndParseSingleResultQuery(ctx, dbClient, query)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/GetParameterTypeValue: error while fetching value for %v: %v", key, err)
}
values = append(values, value)
}
return &GetParameterTypeValueResponse{Types: types, Values: values}, nil
}
type PhysicalBackupDeleteRequest struct {
BackupTag string
LocalPath string
GcsPath string
}
// PhysicalBackupDelete deletes backup data on local or GCS.
func PhysicalBackupDelete(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req PhysicalBackupDeleteRequest) error {
klog.InfoS("config_agent_helpers/PhysicalBackupDelete", "namespace", namespace, "instName", instName, "backupTag", req.BackupTag, "localPath", req.LocalPath, "gcsPath", req.GcsPath)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/PhysicalBackupDelete: failed to create database daemon client: %v", err)
}
defer closeConn()
if err := backup.PhysicalBackupDelete(ctx, &backup.Params{
Client: dbClient,
LocalPath: req.LocalPath,
GCSPath: req.GcsPath,
BackupTag: req.BackupTag,
}); err != nil {
return fmt.Errorf("config_agent_helpers/PhysicalBackupDelete: failed to delete physical backup: %v", err)
}
return nil
}
type PhysicalBackupMetadataRequest struct {
BackupTag string
}
type PhysicalBackupMetadataResponse struct {
BackupScn string
BackupIncarnation string
BackupTimestamp *timestamppb.Timestamp
}
// PhysicalBackupMetadata fetches backup scn/timestamp/incarnation with provided backup tag.
func PhysicalBackupMetadata(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req PhysicalBackupMetadataRequest) (*PhysicalBackupMetadataResponse, error) {
klog.InfoS("config_agent_helpers/PhysicalBackupMetadata", "namespace", namespace, "instName", instName, "backupTag", req.BackupTag)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to create database daemon client: %v", err)
}
defer closeConn()
// Find the max "Next SCN" in current archivelog backup, this will be the backup scn.
// Example of list backup of archivelog output:
// Thrd Seq Low SCN Low Time Next SCN Next Time
// ---- ------- ---------- --------- ---------- ---------
// 1 1 1527386 30-JUL-21 1530961 30-JUL-21
listArchiveLogBackupCmd := "list backup of archivelog all tag '%s';"
res, err := dbClient.RunRMAN(ctx, &dbdpb.RunRMANRequest{Scripts: []string{fmt.Sprintf(listArchiveLogBackupCmd, req.BackupTag)}})
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to list backup of archivelog: %v", err)
}
var threeLinesBuffer [3]string
maxSCN := int64(-1)
scanner := bufio.NewScanner(strings.NewReader(res.GetOutput()[0]))
for scanner.Scan() {
threeLinesBuffer[0] = threeLinesBuffer[1]
threeLinesBuffer[1] = threeLinesBuffer[2]
threeLinesBuffer[2] = scanner.Text()
if strings.Contains(threeLinesBuffer[0], "Next SCN") {
fields := strings.Fields(threeLinesBuffer[2])
if len(fields) != 6 {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: unexpected number of fields: %v", threeLinesBuffer[2])
}
currentSCN, err := strconv.ParseInt(fields[4], 10, 64)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to parse 'Next SCN' %v: %v", fields[2], err)
}
if currentSCN > maxSCN {
maxSCN = currentSCN
}
}
}
if maxSCN < 0 {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to find backup scn")
}
scnToTimestampSQL := "select to_char(scn_to_timestamp(%s) at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as backuptime from dual"
backupTimeResp, err := fetchAndParseSingleResultQuery(ctx, dbClient, fmt.Sprintf(scnToTimestampSQL, strconv.FormatInt(maxSCN, 10)))
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to query backup time: %s", err)
}
if backupTimeResp == "" {
return nil, nil
}
backupTime, err := time.Parse(time.RFC3339, backupTimeResp)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to parse backup time: %s", err)
}
incResp, err := FetchDatabaseIncarnation(ctx, r, dbClientFactory, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/PhysicalBackupMetadata: failed to query database incarnation: %s", err)
}
klog.InfoS("config_agent_helpers/PhysicalBackupMetadata", "backup incarnation", incResp.Incarnation, "backup scn", maxSCN, "backup time", backupTime)
return &PhysicalBackupMetadataResponse{
BackupIncarnation: incResp.Incarnation,
BackupScn: strconv.FormatInt(maxSCN, 10),
BackupTimestamp: timestamppb.New(backupTime),
}, nil
}
type FetchDatabaseIncarnationResponse struct {
Incarnation string
}
// FetchDatabaseIncarnation fetches the database incarnation number.
func FetchDatabaseIncarnation(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string) (*FetchDatabaseIncarnationResponse, error) {
klog.InfoS("config_agent_helpers/FetchDatabaseIncarnation", "namespace", namespace, "instName", instName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
defer func() { _ = closeConn() }()
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/FetchDatabaseIncarnation: failed to create database daemon client: %w", err)
}
inc, err := fetchAndParseSingleResultQuery(ctx, dbClient, consts.GetDatabaseIncarnationSQL)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/FetchDatabaseIncarnation: failed to query database incarnation: %s", err)
}
return &FetchDatabaseIncarnationResponse{Incarnation: inc}, nil
}
type VerifyStandbySettingsRequest struct {
PrimaryHost string
PrimaryPort int32
PrimaryService string
PrimaryUser string
PrimaryCredential *Credential
StandbyDbUniqueName string
StandbyCdbName string
BackupGcsPath string
PasswordFileGcsPath string
StandbyVersion string
}
type VerifyStandbySettingsResponse struct {
Errors []*standbyhelpers.StandbySettingErr
}
type Credential struct {
// Types that are assignable to Source:
// *Credential_GsmSecretReference
Source isCredentialSource
}
func (x *Credential) GetGsmSecretReference() *GsmSecretReference {
if x, ok := x.Source.(*CredentialGsmSecretReference); ok {
return x.GsmSecretReference
}
return nil
}
type isCredentialSource interface {
isCredentialSource()
}
type CredentialGsmSecretReference struct {
GsmSecretReference *GsmSecretReference
}
func (*CredentialGsmSecretReference) isCredentialSource() {}
// VerifyStandbySettings does preflight checks on standby settings.
func VerifyStandbySettings(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req VerifyStandbySettingsRequest) (*VerifyStandbySettingsResponse, error) {
klog.InfoS("config_agent_helpers/VerifyStandbySettings", "namespace", namespace, "instName", instName, "primaryHost", req.PrimaryHost, "standbyDbUniqueName", req.StandbyDbUniqueName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/VerifyStandbySettings: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
sa := secret.NewGSMSecretAccessor(
req.PrimaryCredential.GetGsmSecretReference().ProjectId,
req.PrimaryCredential.GetGsmSecretReference().SecretId,
req.PrimaryCredential.GetGsmSecretReference().Version,
)
defer sa.Clear()
primaryDB := &standby.Primary{
Host: req.PrimaryHost,
Port: int(req.PrimaryPort),
Service: req.PrimaryService,
User: req.PrimaryUser,
PasswordAccessor: sa,
}
standbyDB := &standby.Standby{
CDBName: req.StandbyCdbName,
DBUniqueName: req.StandbyDbUniqueName,
Version: req.StandbyVersion,
}
settingErrs := standby.VerifyStandbySettings(ctx, primaryDB, standbyDB, req.PasswordFileGcsPath, req.BackupGcsPath, dbClient)
//the returned error is always nil because all the errors that occurred during the verification have been added in settingErrs.
return &VerifyStandbySettingsResponse{
Errors: settingErrs,
}, nil
}
type CreateStandbyRequest struct {
PrimaryHost string
PrimaryPort int32
PrimaryService string
PrimaryUser string
PrimaryCredential *Credential
StandbyDbUniqueName string
StandbyLogDiskSize int64
StandbyDbDomain string
BackupGcsPath string
LroInput *LROInput
}
// CreateStandby creates a standby database.
func CreateStandby(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req CreateStandbyRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpers/CreateStandby",
"namespace", namespace,
"instName", instName,
"primaryHost", req.PrimaryHost,
"primaryPort", req.PrimaryPort,
"primaryService", req.PrimaryService,
"primaryUser", req.PrimaryUser,
"standbyDbUniqueName", req.StandbyDbUniqueName,
)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CreateStandby: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
sa := secret.NewGSMSecretAccessor(
req.PrimaryCredential.GetGsmSecretReference().ProjectId,
req.PrimaryCredential.GetGsmSecretReference().SecretId,
req.PrimaryCredential.GetGsmSecretReference().Version,
)
defer sa.Clear()
primaryDB := &standby.Primary{
Host: req.PrimaryHost,
Port: int(req.PrimaryPort),
Service: req.PrimaryService,
User: req.PrimaryUser,
PasswordAccessor: sa,
}
standbyDB := &standby.Standby{
DBUniqueName: req.StandbyDbUniqueName,
Port: consts.SecureListenerPort,
DBDomain: req.StandbyDbDomain,
LogDiskSize: req.StandbyLogDiskSize,
}
lro, err := standby.CreateStandby(ctx, primaryDB, standbyDB, req.BackupGcsPath, req.LroInput.OperationId, dbClient)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/CreateStandby: failed to create standby: %v", err)
}
return lro, nil
}
type SetUpDataGuardRequest struct {
PrimaryHost string
PrimaryPort int32
PrimaryService string
PrimaryUser string
PrimaryCredential *Credential
StandbyDbUniqueName string
StandbyHost string
PasswordFileGcsPath string
}
// SetUpDataGuard updates Data Guard configuration.
func SetUpDataGuard(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req SetUpDataGuardRequest) error {
klog.InfoS("config_agent_helpers/SetupDataGuard",
"namespace", namespace,
"instName", instName,
"primaryHost", req.PrimaryHost,
"primaryPort", req.PrimaryPort,
"primaryService", req.PrimaryService,
"primaryUser", req.PrimaryUser,
"standbyDbUniqueName", req.StandbyDbUniqueName,
"standbyHost", req.StandbyHost,
)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/SetupDataGuard: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
sa := secret.NewGSMSecretAccessor(
req.PrimaryCredential.GetGsmSecretReference().ProjectId,
req.PrimaryCredential.GetGsmSecretReference().SecretId,
req.PrimaryCredential.GetGsmSecretReference().Version,
)
defer sa.Clear()
primaryDB := &standby.Primary{
Host: req.PrimaryHost,
Port: int(req.PrimaryPort),
Service: req.PrimaryService,
User: req.PrimaryUser,
PasswordAccessor: sa,
}
standbyDB := &standby.Standby{
DBUniqueName: req.StandbyDbUniqueName,
Host: req.StandbyHost,
Port: consts.SecureListenerPort,
}
if err := standby.SetUpDataGuard(ctx, primaryDB, standbyDB, req.PasswordFileGcsPath, dbClient); err != nil {
return fmt.Errorf("failed to set up Data Guard: %v", err)
}
return nil
}
type PromoteStandbyRequest struct {
PrimaryHost string
PrimaryPort int32
PrimaryService string
PrimaryUser string
PrimaryCredential *Credential
StandbyDbUniqueName string
StandbyHost string
}
// PromoteStandby promotes standby database to primary.
func PromoteStandby(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req PromoteStandbyRequest) error {
klog.InfoS("config_agent_helpers/PromoteStandby",
"namespace", namespace,
"instName", instName,
"primaryHost", req.PrimaryHost,
"primaryPort", req.PrimaryPort,
"primaryService", req.PrimaryService,
"primaryUser", req.PrimaryUser,
"standbyDbUniqueName", req.StandbyDbUniqueName,
"standbyHost", req.StandbyHost,
)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return fmt.Errorf("config_agent_helpers/PromoteStandby: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
sa := secret.NewGSMSecretAccessor(
req.PrimaryCredential.GetGsmSecretReference().ProjectId,
req.PrimaryCredential.GetGsmSecretReference().SecretId,
req.PrimaryCredential.GetGsmSecretReference().Version,
)
defer sa.Clear()
primaryDB := &standby.Primary{
Host: req.PrimaryHost,
Port: int(req.PrimaryPort),
Service: req.PrimaryService,
User: req.PrimaryUser,
PasswordAccessor: sa,
}
standbyDB := &standby.Standby{
DBUniqueName: req.StandbyDbUniqueName,
}
if err := standby.PromoteStandby(ctx, primaryDB, standbyDB, dbClient); err != nil {
return fmt.Errorf("failed to promote standby: %v", err)
}
return nil
}
type DataGuardStatusRequest struct {
StandbyDbUniqueName string
}
type DataGuardStatusResponse struct {
Output []string
}
// DataGuardStatus returns Data Guard configuration status and standby DB status.
func DataGuardStatus(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req DataGuardStatusRequest) (*DataGuardStatusResponse, error) {
klog.InfoS("config_agent_helpers/DataGuardStatus", "namespace", namespace, "instName", instName, "standbyDbUniqueName", req.StandbyDbUniqueName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/DataGuardStatus: failed to create database daemon dbdClient: %v", err)
}
defer closeConn()
output, err := standby.DataGuardStatus(ctx, req.StandbyDbUniqueName, dbClient)
return &DataGuardStatusResponse{
Output: output,
}, err
}
type ApplyDataPatchRequest struct {
LroInput *LROInput
}
// ApplyDataPatch calls dbdaemon->ApplyDataPatch()
func ApplyDataPatch(ctx context.Context, r client.Reader, dbClientFactory DatabaseClientFactory, namespace, instName string, req ApplyDataPatchRequest) (*lropb.Operation, error) {
klog.InfoS("config_agent_helpersApplyDataPatch", "namespace", namespace, "instName", instName)
dbClient, closeConn, err := dbClientFactory.New(ctx, r, namespace, instName)
if err != nil {
return nil, fmt.Errorf("config_agent_helpers/ApplyDataPatch: failed to create database daemon client: %w", err)
}
defer func() { _ = closeConn() }()
return dbClient.ApplyDataPatchAsync(ctx, &dbdpb.ApplyDataPatchAsyncRequest{
LroInput: &dbdpb.LROInput{
OperationId: req.LroInput.OperationId,
},
})
}