oracle/pkg/database/dbdaemonproxy/dbdaemon_proxy.go (371 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 dbdaemonproxy provides access to the database container.
// From the security standpoint only the following requests are honored:
// - only requests from a localhost
// - only requests against predefined database and listener(s)
// - only for tightly controlled commands
//
// All requests are to be logged and audited.
//
// Only New and CheckDatabaseState functions of this package can be called
// at the instance (aka CDB) provisioning time. The rest of the functions
// are expected to be called only when a database (aka PDB) is provisioned.
package dbdaemonproxy
import (
"context"
"database/sql"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"github.com/godror/godror" // Register database/sql driver
"k8s.io/klog/v2"
"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/database/provision"
)
// Override library functions for the benefit of unit tests.
var (
lsnrctl = func(databaseHome string) string {
return filepath.Join(databaseHome, "bin", "lsnrctl")
}
rman = func(databaseHome string) string {
return filepath.Join(databaseHome, "bin", "rman")
}
orapwd = func(databaseHome string) string {
return filepath.Join(databaseHome, "bin", "orapwd")
}
dbca = func(databaseHome string) string {
return filepath.Join(databaseHome, "bin", "dbca")
}
nid = func(databaseHome string) string {
return filepath.Join(databaseHome, "bin", "nid")
}
sqlOpen = func(driverName, dataSourceName string) (database, error) {
return sql.Open(driverName, dataSourceName)
}
godrorDriverConn = func(ctx context.Context, ex godror.Execer) (conn, error) {
return godror.DriverConn(ctx, ex)
}
makecmd = "/usr/bin/make"
)
// osUtil was defined for tests.
type osUtil interface {
runCommand(bin string, params []string) error
}
type osUtilImpl struct {
}
func (o *osUtilImpl) runCommand(bin string, params []string) error {
ohome := os.Getenv("ORACLE_HOME")
klog.InfoS("executing command with args", "cmd", bin, "params", params, "ORACLE_SID", os.Getenv("ORACLE_SID"), "ORACLE_HOME", ohome, "TNS_ADMIN", os.Getenv("TNS_ADMIN"))
switch bin {
case lsnrctl(ohome), rman(ohome), orapwd(ohome), dbca(ohome), nid(ohome), makecmd:
default:
return fmt.Errorf("command %q is not supported", bin)
}
cmd := exec.Command(bin)
cmd.Args = append(cmd.Args, params...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("command %q failed at path %q with args %v: %v", bin, cmd.Path, cmd.Args, err)
}
return nil
}
// database defines the sql.DB APIs, which will be used in this package
type database interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
Close() error
}
// conn defines the godror.Conn APIs, which will be used in this package
type conn interface {
Startup(godror.StartupMode) error
Shutdown(godror.ShutdownMode) error
}
// Server holds a database config.
type Server struct {
*dbdpb.UnimplementedDatabaseDaemonProxyServer
hostName string
databaseSid *syncState
databaseHome string
pdbConnStr string
osUtil osUtil
version string
}
func (s Server) String() string {
pdbConnStr := s.pdbConnStr
if pdbConnStr != "" {
pdbConnStr = "<REDACTED>"
}
return fmt.Sprintf("{hostName=%q, databaseSid=%+v, databaseHome=%q, pdbConnStr=%q}", s.hostName, s.databaseSid, s.databaseHome, pdbConnStr)
}
type syncState struct {
sync.RWMutex
val string
}
// shutdownDatabase performs a database shutdown in a requested <mode>.
// It always connects to the local database.
// Set ORACLE_HOME and ORACLE_SID in the env to control the target database.
// A caller may decide to ignore ORA-1034 and just log a warning
// if a database has already been down (or raise an error if appropriate)..
func (s *Server) shutdownDatabase(ctx context.Context, mode godror.ShutdownMode) error {
// Consider allowing PRELIM mode connections for SHUTDOWN ABORT mode.
// This is useful when the server has maxed out on connections.
db, err := sqlOpen("godror", "oracle://?sysdba=1")
if err != nil {
klog.ErrorS(err, "dbdaemon/shutdownDatabase: failed to connect to a database")
return err
}
defer db.Close()
oraDB, err := godrorDriverConn(ctx, db)
if err != nil {
return err
}
if err := oraDB.Shutdown(mode); err != nil {
return err
}
// The shutdown process is over after the first Shutdown call in ABORT
// mode.
if mode == godror.ShutdownAbort {
return err
}
_, err = db.ExecContext(ctx, "alter database close normal")
if err != nil && strings.Contains(err.Error(), "ORA-01507:") {
klog.InfoS("dbdaemon/shutdownDatabase: database is already closed", "err", err)
err = nil
}
if err != nil {
return err
}
_, err = db.ExecContext(ctx, "alter database dismount")
if err != nil && strings.Contains(err.Error(), "ORA-01507:") {
klog.InfoS("dbdaemon/shutdownDatabase: database is already dismounted", "err", err)
err = nil
}
if err != nil {
return err
}
return oraDB.Shutdown(godror.ShutdownFinal)
}
// startupDatabase performs a database startup in a requested mode.
// godror.StartupMode controls FORCE/RESTRICT options.
// databaseState string controls NOMOUNT/MOUNT/OPEN options.
// Setting a pfile to use on startup is currently unsupported.
// It always connects to the local database.
// Set ORACLE_HOME and ORACLE_SID in the env to control the target database.
func (s *Server) startupDatabase(ctx context.Context, mode godror.StartupMode, state string) error {
// To startup a shutdown database, open a prelim connection.
db, err := sqlOpen("godror", "oracle://?sysdba=1&prelim=1")
if err != nil {
return err
}
defer db.Close()
oraDB, err := godrorDriverConn(ctx, db)
if err != nil {
return err
}
if err := oraDB.Startup(mode); err != nil {
return err
}
if strings.ToLower(state) == "nomount" {
return nil
}
// To finish mounting/opening, open a normal connection.
db2, err := sqlOpen("godror", "oracle://?sysdba=1")
if err != nil {
return err
}
defer db2.Close()
if _, err := db2.ExecContext(ctx, "alter database mount"); err != nil {
return err
}
if strings.ToLower(state) == "mount" {
return nil
}
_, err = db2.ExecContext(ctx, "alter database open")
return err
}
// BounceDatabase is a Database Daemon method to start or stop a database.
func (s *Server) BounceDatabase(ctx context.Context, req *dbdpb.BounceDatabaseRequest) (*dbdpb.BounceDatabaseResponse, error) {
klog.InfoS("dbdaemon/BounceDatabase", "req", req, "serverObj", s)
reqDatabaseName := req.GetDatabaseName()
var ls dbdpb.DatabaseState
var operation string
// Allowed commands: startup [nomount|mount|open|force_nomount] or shutdown [immediate|transactional|abort].
validStartupOptions := map[string]bool{"nomount": true, "mount": true, "open": true, "force_nomount": true}
// validShutdownOptions keys should match shutdownEnumMap below to prevent nil.
validShutdownOptions := map[string]bool{"immediate": true, "transactional": true, "abort": true}
switch req.Operation {
case dbdpb.BounceDatabaseRequest_STARTUP:
ls = dbdpb.DatabaseState_READY
if req.Option != "" && !validStartupOptions[req.Option] {
e := []string{fmt.Sprintf("illegal option %q requested for operation %q", req.Option, req.Operation)}
return &dbdpb.BounceDatabaseResponse{
DatabaseState: dbdpb.DatabaseState_DATABASE_STATE_ERROR,
ErrorMsg: e,
}, nil
}
operation = "startup"
case dbdpb.BounceDatabaseRequest_SHUTDOWN:
ls = dbdpb.DatabaseState_STOPPED
if req.Option != "" && !validShutdownOptions[req.Option] {
e := []string{fmt.Sprintf("illegal option %q requested for operation %q", req.Option, req.Operation)}
return &dbdpb.BounceDatabaseResponse{
DatabaseState: dbdpb.DatabaseState_DATABASE_STATE_ERROR,
ErrorMsg: e,
}, nil
}
operation = "shutdown"
default:
return nil, fmt.Errorf("illegal operation requested: %q", req.Operation)
}
// Add lock to protect server state "databaseSid" and os env variable "ORACLE_SID".
// When bouncing the DB, DB is not ready to run cmds or SQLs, it seems to be ok to block all other APIs for now.
s.databaseSid.Lock()
defer s.databaseSid.Unlock()
// Sets env to bounce a database, needed for start and shutdown.
os.Setenv("ORACLE_SID", reqDatabaseName)
var err error
shutdownEnumMap := map[string]godror.ShutdownMode{
"immediate": godror.ShutdownImmediate,
"transactional": godror.ShutdownTransactional,
"abort": godror.ShutdownAbort,
}
if operation == "shutdown" {
// shutdownEnumMap keys should match validShutdownOptions above to prevent nil.
err = s.shutdownDatabase(ctx, shutdownEnumMap[req.Option])
if err != nil && strings.Contains(err.Error(), "ORA-01034:") {
klog.InfoS("dbdaemon/shutdownDatabase: database is already down", "err", err)
err = nil
}
} else { // startup
switch req.Option {
case "force_nomount":
err = s.startupDatabase(ctx, godror.StartupForce, "nomount")
default:
err = s.startupDatabase(ctx, godror.StartupDefault, req.Option)
}
}
return &dbdpb.BounceDatabaseResponse{
DatabaseState: ls,
ErrorMsg: nil,
}, err
}
func (s *Server) runCommand(bin string, params []string) error {
// Sets env to bounce a database|listener.
os.Setenv("ORACLE_SID", s.databaseSid.val)
return s.osUtil.runCommand(bin, params)
}
// BounceListener is a Database Daemon method to start or stop a listener.
func (s *Server) BounceListener(_ context.Context, req *dbdpb.BounceListenerRequest) (*dbdpb.BounceListenerResponse, error) {
klog.InfoS("dbdaemon/BounceListener", "req", req, "serverObj", s)
var ls dbdpb.ListenerState
var operation string
switch req.Operation {
case dbdpb.BounceListenerRequest_START:
ls = dbdpb.ListenerState_UP
operation = "start"
case dbdpb.BounceListenerRequest_STOP:
ls = dbdpb.ListenerState_DOWN
operation = "stop"
default:
return nil, fmt.Errorf("illegal operation %q requested for listener %q", req.Operation, req.ListenerName)
}
// Add lock to protect server state "databaseSid" and os env variable "ORACLE_SID".
s.databaseSid.RLock()
defer s.databaseSid.RUnlock()
os.Setenv("TNS_ADMIN", req.TnsAdmin)
bin := lsnrctl(s.databaseHome)
params := []string{operation, req.ListenerName}
if err := s.runCommand(bin, params); err != nil {
return nil, fmt.Errorf(fmt.Sprintf("a listener %q command %q failed: %v", req.ListenerName, req.Operation, err))
}
klog.InfoS("dbdaemon/BounceListener done", "req", req)
return &dbdpb.BounceListenerResponse{
ListenerState: ls,
ErrorMsg: nil,
}, nil
}
// ProxyRunDbca execute the command to create a database instance
func (s *Server) ProxyRunDbca(ctx context.Context, req *dbdpb.ProxyRunDbcaRequest) (*dbdpb.ProxyRunDbcaResponse, error) {
s.databaseSid.Lock()
defer s.databaseSid.Unlock()
klog.InfoS("proxy/ProxyRunDbca: Removing Oracle config files softlinks...")
if err := provision.RemoveConfigFileLinks(req.GetOracleHome(), req.GetDatabaseName()); err != nil {
return nil, err
}
klog.InfoS("proxy/ProxyRunDbca: Running dbca...")
if err := s.osUtil.runCommand(dbca(req.GetOracleHome()), req.GetParams()); err != nil {
return nil, fmt.Errorf("dbca cmd failed: %v", err)
}
klog.InfoS("proxy/ProxyRunDbca: Initializing environment for Oracle...")
if err := initializeEnvironment(s, req.GetOracleHome(), req.GetDatabaseName()); err != nil {
return nil, err
}
klog.InfoS("proxy/ProxyRunDbca: Moving Oracle config files...")
if err := provision.MoveConfigFiles(req.GetOracleHome(), req.GetDatabaseName()); err != nil {
return nil, err
}
klog.InfoS("proxy/ProxyRunDbca: Creating symlinks to Oracle config files...")
if err := provision.RelinkConfigFiles(req.GetOracleHome(), req.GetDatabaseName()); err != nil {
return nil, err
}
klog.InfoS("proxy/ProxyRunDbca: DONE")
return &dbdpb.ProxyRunDbcaResponse{}, nil
}
// ProxyRunNID execute the command to rename a database instance
func (s *Server) ProxyRunNID(ctx context.Context, req *dbdpb.ProxyRunNIDRequest) (*dbdpb.ProxyRunNIDResponse, error) {
s.databaseSid.Lock()
defer s.databaseSid.Unlock()
if err := s.osUtil.runCommand(nid(s.databaseHome), req.GetParams()); err != nil {
return nil, fmt.Errorf("nid cmd failed: %v", err)
}
s.databaseSid.val = req.DestDbName
// We need to regenerate the env file with the new db name
if err := createDotEnv(s.databaseHome, s.version, s.databaseSid.val); err != nil {
return nil, err
}
klog.InfoS("proxy/ProxyRunNID: DONE")
return &dbdpb.ProxyRunNIDResponse{}, nil
}
// ProxyRunInitOracle execute the init_oracle binary with input params
func (s *Server) ProxyRunInitOracle(ctx context.Context, req *dbdpb.ProxyRunInitOracleRequest) (*dbdpb.ProxyRunInitOracleResponse, error) {
cmd := exec.Command("./agents/init_oracle",
req.GetParams()...)
out, err := cmd.CombinedOutput()
klog.Infof("proxy/ProxyRunInitOracle: init_oracle log: \n %s", string(out))
if err != nil {
klog.InfoS("proxy/ProxyRunInitOracle: FAIL")
return nil, fmt.Errorf("init_oracle failed: %v", err)
}
klog.InfoS("proxy/ProxyRunInitOracle: DONE")
return &dbdpb.ProxyRunInitOracleResponse{}, nil
}
// ProxyFetchServiceImageMetaData returns metadata from the container running the oracledb container
func (s *Server) ProxyFetchServiceImageMetaData(ctx context.Context, req *dbdpb.ProxyFetchServiceImageMetaDataRequest) (*dbdpb.ProxyFetchServiceImageMetaDataResponse, error) {
oracleHome, cdbName, version, err := provision.FetchMetaDataFromImage()
if err != nil {
klog.Error("proxy/FetchServiceImageMetaData: FAILED")
return nil, fmt.Errorf("could not fetch image metadata: %v", err)
}
var seededImage bool
if _, err := os.Stat(consts.SeededImageFile); err == nil {
seededImage = true
} else if _, err := os.Stat(consts.UnseededImageFile); err == nil {
seededImage = false
} else {
klog.Error("proxy/FetchServiceImageMetaData: FAILED")
return nil, fmt.Errorf("could not determine if image is seeded or not: %v", err)
}
return &dbdpb.ProxyFetchServiceImageMetaDataResponse{Version: version, CdbName: cdbName, OracleHome: oracleHome, SeededImage: seededImage}, nil
}
func (s *Server) SetDnfsState(ctx context.Context, req *dbdpb.SetDnfsStateRequest) (*dbdpb.SetDnfsStateResponse, error) {
oracleHome := os.Getenv("ORACLE_HOME")
enable := "dnfs_on"
if !req.Enable {
enable = "dnfs_off"
}
params := []string{
"-f",
fmt.Sprintf("%s/rdbms/lib/%s", oracleHome, "ins_rdbms.mk"),
enable,
}
if err := s.runCommand("/usr/bin/make", params); err != nil {
msg := "dbdaemon/SetDnfsState error while running dNFS turning on command"
klog.ErrorS(err, msg, "request", req)
return nil, fmt.Errorf(msg)
}
return &dbdpb.SetDnfsStateResponse{}, nil
}
// initializeEnvironment sets Oracle specific environment variables, creates
// the .env file.
func initializeEnvironment(s *Server, home string, dbName string) error {
s.databaseHome = home
s.databaseSid.val = dbName
if err := createDotEnv(home, s.version, dbName); err != nil {
return err
}
return nil
}
func createDotEnv(dbHome, dbVersion, dbName string) error {
dotEnvFileName := fmt.Sprintf("%s/%s.env", consts.OracleDir, dbName)
dotEnvFile, err := os.Create(dotEnvFileName)
if err != nil {
return err
}
dotEnvFile.WriteString(fmt.Sprintf("export ORACLE_HOME=%s\n", dbHome))
dotEnvFile.WriteString(fmt.Sprintf("export ORACLE_BASE=%s\n", os.Getenv("ORACLE_BASE")))
dotEnvFile.WriteString(fmt.Sprintf("export ORACLE_SID=%s\n", dbName))
dotEnvFile.WriteString(fmt.Sprintf("export PATH=%s/bin:%s/OPatch:/usr/local/bin:/usr/local/sbin:/sbin:/bin:/usr/sbin:/usr/bin:/root/bin\n", dbHome, dbHome))
dotEnvFile.WriteString(fmt.Sprintf("export LD_LIBRARY_PATH=%s/lib:/usr/lib\n", dbHome))
return dotEnvFile.Close()
}
// New creates a new Database Daemon Server object.
// It first gets called on a CDB provisioning and at this time
// a PDB name is not known yet (to be supplied via a separate call).
func New(hostname, cdbNameFromYaml string) (*Server, error) {
oracleHome, _, version, err := provision.FetchMetaDataFromImage()
s := &Server{hostName: hostname, osUtil: &osUtilImpl{}, databaseSid: &syncState{}, version: version}
if err != nil {
return nil, fmt.Errorf("error while fetching metadata from image: %v", err)
}
klog.Infof("Initializing environment for Oracle...")
err = initializeEnvironment(s, oracleHome, cdbNameFromYaml)
if err != nil {
return nil, fmt.Errorf("an error occured while initializing the environment for Oracle: %v", err)
}
if _, err := os.Stat(consts.UnseededImageFile); err == nil {
if err := provision.RelinkConfigFiles(oracleHome, cdbNameFromYaml); err != nil {
return nil, err
}
}
return s, nil
}