oracle/pkg/database/provision/common.go (329 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 provision import ( "bytes" "context" "fmt" "io" "io/ioutil" "os" "os/user" "path/filepath" "strconv" "strings" "text/template" "k8s.io/klog/v2" "github.com/GoogleCloudPlatform/elcarro-oracle-operator/oracle/pkg/agents/consts" ) var ( // ListenerTemplateName is the filepath for the listener file template in the container. ListenerTemplateName = filepath.Join(consts.ScriptDir, "bootstrap-database-listener.template") // TnsnamesTemplateName is the filepath for the tnsnames file template in the container. TnsnamesTemplateName = filepath.Join(consts.ScriptDir, "bootstrap-database-tnsnames.template") // ControlFileTemplateName is the filepath for the control file template in the container. ControlFileTemplateName = filepath.Join(consts.ScriptDir, "bootstrap-database-crcf.template") // InitOraTemplateName is the filepath for the initOra file template in the container for Oracle EE/SE. InitOraTemplateName = filepath.Join(consts.ScriptDir, "bootstrap-database-initfile.template") // InitOraXeTemplateName is the filepath for the initOra file template in the container for Oracle 18c XE. InitOraXeTemplateName = filepath.Join(consts.ScriptDir, "bootstrap-database-initfile-oracle-xe.template") fileSQLNet = "sqlnet.ora" // SQLNetSrc is the filepath for the control file template in the container. SQLNetSrc = filepath.Join(consts.ScriptDir, fileSQLNet) ) // ListenerInput is the struct, which will be applied to the listener template. type ListenerInput struct { PluggableDatabaseNames []string DatabaseName string DatabaseBase string DatabaseHome string ListenerName string ListenerPort string ListenerProtocol string DatabaseHost string DBDomain string CDBServiceName string } type controlfileInput struct { DatabaseName string DataFilesDir string DataFilesMultiLine string } // oracleDB defines APIs for the DB information provider. // Information provider need implement this interface to support oracle DB task. type oracleDB interface { // GetVersion returns the version of the oracle DB. GetVersion() string // GetDataFilesDir returns data files directory location. GetDataFilesDir() string // GetSourceDataFilesDir returns data files directory location of the pre-built DB. GetSourceDataFilesDir() string // GetConfigFilesDir returns config file directory location. GetConfigFilesDir() string // GetSourceConfigFilesDir returns config file directory location of the pre-built DB. GetSourceConfigFilesDir() string // GetAdumpDir returns adump directory location. GetAdumpDir() string // GetCdumpDir returns cdump directory location. GetCdumpDir() string // GetFlashDir returns flash directory location. GetFlashDir() string // GetListenerDir returns listeners directory location. GetListenerDir() string // GetDatabaseBase returns oracle base location. GetDatabaseBase() string // GetDatabaseName returns database name. GetDatabaseName() string // GetSourceDatabaseName returns database name of the pre-built DB. GetSourceDatabaseName() string // GetDatabaseHome returns database home location. GetDatabaseHome() string // GetDataFiles returns initial data files associated with the DB. GetDataFiles() []string // GetSourceConfigFiles returns initial config files associated with pre-built DB. GetSourceConfigFiles() []string // GetConfigFiles returns config files associated with current DB. GetConfigFiles() []string // GetMountPointDatafiles returns the mount point of the data files. GetMountPointDatafiles() string // GetMountPointAdmin returns the mount point of the admin directory. GetMountPointAdmin() string // GetListeners returns listeners of the DB. GetListeners() map[string]*consts.Listener // GetDatabaseUniqueName returns database unique name. GetDatabaseUniqueName() string // GetDBDomain returns DB domain. GetDBDomain() string // GetMountPointDiag returns the mount point of the diag directory. GetMountPointDiag() string // GetDatabaseParamPGATargetMB returns PGA value in MB. GetDatabaseParamPGATargetMB() uint64 // GetDatabaseParamSGATargetMB returns SGA value in MB. GetDatabaseParamSGATargetMB() uint64 // GetOratabFile returns oratab file location. GetOratabFile() string // GetSourceDatabaseHost returns host name of the pre-built DB. GetSourceDatabaseHost() string // GetHostName returns host name. GetHostName() string // GetCreateUserCmds returns create user commands to setup users. GetCreateUserCmds() []*createUser // IsCDB returns true if this is a cdb. IsCDB() bool } // createUser provides user name and sql commands to create the user in this DB. type createUser struct { user string cmds []string } // osUtil was added for unit test. type osUtil interface { Lookup(username string) (*user.User, error) LookupGroup(name string) (*user.Group, error) } // OSUtilImpl contains utility methods for fetching user/group metadata. type OSUtilImpl struct{} // Lookup method obtains the user's metadata (uid, gid, username, name, homedir). func (*OSUtilImpl) Lookup(username string) (*user.User, error) { return user.Lookup(username) } // LookupGroup method obtains the group's metadata (gid, name). func (*OSUtilImpl) LookupGroup(name string) (*user.Group, error) { return user.LookupGroup(name) } type task interface { GetName() string Call(ctx context.Context) error } // simpleTask is a task which should be testable. Task should bring the system // to a state which can be verified. type simpleTask struct { name string callFun func(ctx context.Context) error } func (task *simpleTask) GetName() string { return task.name } func (task *simpleTask) Call(ctx context.Context) error { return task.callFun(ctx) } func doSubTasks(ctx context.Context, parentTaskName string, subTasks []task) error { klog.InfoS("parent task: running", "task", parentTaskName) for _, sub := range subTasks { klog.InfoS("subtask: running", "parent task", parentTaskName, "sub task", sub.GetName()) if err := sub.Call(ctx); err != nil { klog.ErrorS(err, "Subtask failed", "parent task", parentTaskName, "sub task", sub.GetName()) return err } klog.InfoS("subtask: Done", "parent task", parentTaskName, "sub task", sub.GetName()) } klog.InfoS("parent task: Done", "task", parentTaskName) return nil } // oracleUser returns uid and gid of the Oracle user. func oracleUser(util osUtil) (uint32, uint32, error) { u, err := util.Lookup(consts.OraUser) if err != nil { return 0, 0, fmt.Errorf("oracleUser: could not determine the current user: %v", err) } if u.Username == "root" { return 0, 0, fmt.Errorf("oracleUser: this program is designed to run by the Oracle software installation owner (e.g. oracle), not %q", u.Username) } // Oracle user's primary group name should be either dba or oinstall. groups := consts.OraGroup var gids []string for _, group := range groups { g, err := util.LookupGroup(group) // Not both groups are mandatory, e.g. oinstall may not exist. klog.InfoS("looking up groups", "group", group, "g", g) if err != nil { continue } gids = append(gids, g.Gid) } for _, g := range gids { if u.Gid == g { usr, err := strconv.ParseUint(u.Uid, 10, 32) if err != nil { return 0, 0, err } grp, err := strconv.ParseUint(u.Gid, 10, 32) if err != nil { return 0, 0, err } return uint32(usr), uint32(grp), nil } } return 0, 0, fmt.Errorf("oracleUser: current user's primary group (GID=%q) is not dba|oinstall (GID=%q)", u.Gid, gids) } // LoadTemplateListener applies listener input to listener and tns template. // It returns listener tns and sqlnet in string. // In contrast to pfile, env file and a control file, there may be multiple listeners // and a search/replace in that file is different, so it's easier to load it while // iterating over listeners, not ahead of time. This method also generates the tnsnames // based on the port numbers of the listeners. func LoadTemplateListener(l *ListenerInput, name, port, protocol string) (string, string, string, error) { l.ListenerName = name l.ListenerPort = port l.ListenerProtocol = protocol t, err := template.New(filepath.Base(ListenerTemplateName)).ParseFiles(ListenerTemplateName) if err != nil { return "", "", "", fmt.Errorf("LoadTemplateListener: parsing %q failed: %v", ListenerTemplateName, err) } listenerBuf := &bytes.Buffer{} if err := t.Execute(listenerBuf, l); err != nil { return "", "", "", fmt.Errorf("LoadTemplateListener: executing %q failed: %v", ListenerTemplateName, err) } tns, err := template.New(filepath.Base(TnsnamesTemplateName)).ParseFiles(TnsnamesTemplateName) if err != nil { return "", "", "", fmt.Errorf("LoadTemplateListener: parsing %q failed: %v", TnsnamesTemplateName, err) } tnsBuf := &bytes.Buffer{} if err := tns.Execute(tnsBuf, l); err != nil { return "", "", "", fmt.Errorf("LoadTemplateListener: executing %q failed: %v", TnsnamesTemplateName, err) } sqlnet, err := ioutil.ReadFile(SQLNetSrc) if err != nil { return "", "", "", fmt.Errorf("initDBListeners: unable to read sqlnet from scripts directory: %v", err) } return listenerBuf.String(), tnsBuf.String(), string(sqlnet), nil } // MakeDirs creates directories in the container. func MakeDirs(ctx context.Context, dirs []string, uid, gid uint32) error { for _, odir := range dirs { if err := os.MkdirAll(odir, 0750); err != nil { return fmt.Errorf("create a directory %q failed: %v", odir, err) } klog.InfoS("created a directory", "dir", odir) } return nil } // replace does a search and replace of term to toterm in place. func replace(outFile, term, toterm string, uid, gid uint32) error { input, err := ioutil.ReadFile(outFile) if err != nil { return fmt.Errorf("replace: reading %q failed: %v", outFile, err) } out := bytes.Replace(input, []byte(term), []byte(toterm), -1) if err := ioutil.WriteFile(outFile, out, 0600); err != nil { return fmt.Errorf("replace: error writing file: %v", err) } return nil } // MoveFile moves a file between directories. // os.Rename() gives error "invalid cross-device link" for Docker container with Volumes. func MoveFile(sourceFile, destFile string) error { klog.Infof("Moving %s to %s", sourceFile, destFile) inputFile, err := os.Open(sourceFile) if err != nil { return fmt.Errorf("couldn't open source file: %s", err) } defer func() { if err := inputFile.Close(); err != nil { klog.Warningf("failed to close $v: %v", inputFile, err) } }() outputFile, err := os.Create(destFile) if err != nil { return fmt.Errorf("couldn't open dest file: %s", err) } defer func() { if err := outputFile.Close(); err != nil { klog.Warningf("failed to close $v: %v", outputFile, err) } }() _, err = io.Copy(outputFile, inputFile) if err != nil { return fmt.Errorf("writing to output file failed: %s", err) } // The copy was successful, so now delete the original file return os.Remove(sourceFile) } // MoveConfigFiles moves Database config files from Oracle standard paths to the // persistent configuration in the PD. func MoveConfigFiles(OracleHome, CDBName string) error { // /u02/app/oracle/oraconfig/<CDBName> configDir := fmt.Sprintf(consts.ConfigDir, consts.DataMount, CDBName) // /u01/app/oracle/product/12.2/db/dbs/ sourceConfigDir := filepath.Join(OracleHome, "dbs") for _, f := range []string{fmt.Sprintf("spfile%s.ora", CDBName), fmt.Sprintf("orapw%s", CDBName)} { sf := filepath.Join(sourceConfigDir, f) tf := filepath.Join(configDir, f) tfDir := filepath.Dir(tf) if err := os.MkdirAll(tfDir, 0750); err != nil { return fmt.Errorf("MoveConfigFiles: failed to create dir %s: %v", tfDir, err) } if err := MoveFile(sf, tf); err != nil { return fmt.Errorf("MoveConfigFiles: move config file %s to %s failed: %v", sf, tf, err) } } return nil } // getConfigFilesMapping returns config files symlink mapping. func getConfigFilesMapping(OracleHome, CDBName string) map[string]string { mapping := make(map[string]string) configDir := fmt.Sprintf(consts.ConfigDir, consts.DataMount, CDBName) sourceConfigDir := filepath.Join(OracleHome, "dbs") for _, f := range []string{fmt.Sprintf("spfile%s.ora", CDBName), fmt.Sprintf("init%s.ora", CDBName), fmt.Sprintf("orapw%s", CDBName)} { link := filepath.Join(sourceConfigDir, f) file := filepath.Join(configDir, f) mapping[link] = file } return mapping } // RelinkConfigFiles creates softlinks under the Oracle standard paths from the // persistent configuration files in the PD. func RelinkConfigFiles(OracleHome, CDBName string) error { if err := RemoveConfigFileLinks(OracleHome, CDBName); err != nil { return fmt.Errorf("RelinkConfigFiles: unable to delete existing links: %v", err) } for link, file := range getConfigFilesMapping(OracleHome, CDBName) { if err := os.Symlink(file, link); err != nil { return fmt.Errorf("RelinkConfigFiles: symlink creation failed from %s to oracle directories %s: %v", link, file, err) } } return nil } // RemoveConfigFileLinks removes softlinks of config files under the Oracle standard path. // Prepare for database creation through DBCA. func RemoveConfigFileLinks(OracleHome, CDBName string) error { for link := range getConfigFilesMapping(OracleHome, CDBName) { if _, err := os.Lstat(link); err == nil { if err := os.Remove(link); err != nil { return fmt.Errorf("RemoveConfigFileLinks: unable to delete existing link %s: %v", link, err) } } else { klog.Infof("RemoveConfigFileLinks: No prior file to cleanup: %v", err) } } return nil } // CreateUserCmd returns sql cmd to create a user with provided identifier. func CreateUserCmd(user, identifier string) string { return fmt.Sprintf("create user %s identified by %s", user, identifier) } // ChangePasswordCmd returns sql cmd to change user identifier. func ChangePasswordCmd(user, newIdentifier string) string { return fmt.Sprintf("alter user %s identified by %s ", user, newIdentifier) } // GrantUserCmd returns sql cmd to grant permissions to a user. // Permissions are either a single permission or a list of permissions separated by comma. func GrantUserCmd(user, permissions string) string { return fmt.Sprintf("grant %s to %s", permissions, user) } // FetchMetaDataFromImage returns Oracle Home, CDB name, Version by parsing // database image metadata file if it exists. Otherwise, environment variables are used. func FetchMetaDataFromImage() (oracleHome, cdbName, version string, err error) { if os.Getenv("ORACLE_SID") != "" { cdbName = os.Getenv("ORACLE_SID") //the existence of the ORACLE_SID env variable isn't enough to conclude that a CDB of that name exists //The existence of an oradata directory containing ORACLE_SID confirms the existence of a CDB of that name if _, err = os.Stat(os.Getenv("ORACLE_BASE") + "/oradata/" + os.Getenv("ORACLE_SID")); os.IsNotExist(err) { //After a database is provisioned, the oradata directory will be located on the DataMount if _, err = os.Stat(fmt.Sprintf(consts.DataDir, consts.DataMount, os.Getenv("ORACLE_SID"))); os.IsNotExist(err) { cdbName = "" } } } return os.Getenv("ORACLE_HOME"), cdbName, getOracleVersionUsingOracleHome(os.Getenv("ORACLE_HOME")), nil } // getVersionUsingOracleHome infers the version of the ORACLE Database installation from the specified ORACLE_HOME path func getOracleVersionUsingOracleHome(oracleHome string) string { tokens := strings.Split(oracleHome, "/") return tokens[len(tokens)-2] } // GetDefaultInitParams returns default init parameters, which will be set in DB creation. func GetDefaultInitParams(dbName string) map[string]string { controlFileLoc := filepath.Join(fmt.Sprintf(consts.DataDir, consts.DataMount, dbName), "control01.ctl") initParamDict := make(map[string]string) initParamDict["log_archive_dest_1"] = "'LOCATION=USE_DB_RECOVERY_FILE_DEST'" initParamDict["enable_pluggable_database"] = "TRUE" initParamDict["common_user_prefix"] = "'gcsql$'" initParamDict["control_files"] = fmt.Sprintf("'%s'", controlFileLoc) return initParamDict } // MapToSlice converts map[string]string into a string slice with format "<key>=<value>". func MapToSlice(kv map[string]string) []string { var result []string for k, v := range kv { result = append(result, fmt.Sprintf("%s=%s", k, v)) } return result } // MergeInitParams merges default parameters and user specified parameters, and // returns merged parameters. func MergeInitParams(defaultParams map[string]string, userParams []string) (map[string]string, error) { mergedParams := make(map[string]string) for _, userParam := range userParams { kv := strings.Split(userParam, "=") if len(kv) != 2 { return nil, fmt.Errorf("MergeInitParam: user param %s is not separated by =", userParam) } klog.InfoS("provision/MergeInitParams: adding user param", "key", kv[0], "val", kv[1]) mergedParams[kv[0]] = kv[1] } // We only support merging of user params and reject any params trying to override our internal setting used by controller. // For example, if we permit overrides and the user tries to reassign common_user_prefix with says xyz instead of the default gcsql$, our health checks will break. for k, v := range defaultParams { if val, ok := mergedParams[k]; ok { klog.InfoS("provision/MergeInitParams: overriding user param", "key", k, "user defined val", val, "override val", v) } mergedParams[k] = v klog.InfoS("provision/MergeInitParams: adding default param", "key", k, "val", v) } return mergedParams, nil }