From 2bef492916420af5526f350e8096ac7508b1b2ce Mon Sep 17 00:00:00 2001 From: Alexey Basinov Date: Thu, 19 Feb 2026 16:26:36 -0800 Subject: [PATCH] Implement oracle_run_datapatch Oracle Agent Handler to support the SQL patching phase of the out-of-place patching workflow. Handler logic: 1. Attempts to open all PDBs using `ALTER PLUGGABLE DATABASE ALL OPEN` to make sure patches are applied to all containers in a CDB. Failures here are logged as non-fatal to support non-CDB setups. 2. Runs the `$ORACLE_HOME/OPatch/datapatch -verbose` to apply SQL-level changes to the database registry. 3. Executes the`@?/rdbms/admin/utlrp` script to recompile any invalid objects. 4. Queries the `dba_registry_sqlpatch` view to log the patch status for debugging. PiperOrigin-RevId: 872616013 --- internal/oraclehandlers/patching.go | 70 ++++++++++++-- internal/oraclehandlers/patching_test.go | 113 ++++++++++++++++++++--- 2 files changed, 166 insertions(+), 17 deletions(-) diff --git a/internal/oraclehandlers/patching.go b/internal/oraclehandlers/patching.go index 293f0ce..1e144f0 100644 --- a/internal/oraclehandlers/patching.go +++ b/internal/oraclehandlers/patching.go @@ -226,13 +226,71 @@ func enableAutostart(ctx context.Context, logger *zap.SugaredLogger, params map[ // RunDatapatch implements the oracle_run_datapatch guest action. func RunDatapatch(ctx context.Context, command *gpb.Command, cloudProperties *metadataserver.CloudProperties) *gpb.CommandResult { - log.CtxLogger(ctx).Info("oracle_run_datapatch handler called") - // TODO: Implement oracle_run_datapatch handler. - return &gpb.CommandResult{ - Command: command, - ExitCode: 1, - Stdout: "oracle_run_datapatch not implemented.", + params := command.GetAgentCommand().GetParameters() + logger := log.CtxLogger(ctx) + if result := validateParams(ctx, logger, command, params, []string{"oracle_sid", "oracle_home", "oracle_user"}); result != nil { + return result } + + logger = logger.With("oracle_sid", params["oracle_sid"], "oracle_home", params["oracle_home"], "oracle_user", params["oracle_user"]) + logger.Info("oracle_run_datapatch handler called") + + if err := runDatapatch(ctx, logger, params); err != nil { + logger.Warnw("RunDatapatch failed", "error", err) + return commandResult(ctx, logger, command, "", "", codepb.Code_INTERNAL, err.Error(), err) + } + + return commandResult(ctx, logger, command, "Datapatch execution completed successfully", "", codepb.Code_OK, "Datapatch execution completed successfully", nil) +} + +func runDatapatch(ctx context.Context, logger *zap.SugaredLogger, params map[string]string) error { + // Open all PDBs. + // We ignore errors here because the database might not be a CDB, or PDBs might already be open. + pdbStdout, pdbStderr, pdbErr := runSQL(ctx, params, "ALTER PLUGGABLE DATABASE ALL OPEN;", 60, false) + if pdbErr != nil { + logger.Warnw("Attempted to open all PDBs", "stdout", pdbStdout, "stderr", pdbStderr, "error", pdbErr) + } else { + logger.Infow("Opened all PDBs", "stdout", pdbStdout) + } + + // Run datapatch. + oracleHome := params["oracle_home"] + oracleUser := params["oracle_user"] + oracleSID := params["oracle_sid"] + datapatchPath := filepath.Join(oracleHome, "OPatch", "datapatch") + + // datapatch can take a significant amount of time. + logger.Info("Executing datapatch...") + datapatchRes := executeCommand(ctx, commandlineexecutor.Params{ + Executable: datapatchPath, + Args: []string{"-verbose"}, + User: oracleUser, + Env: []string{"ORACLE_HOME=" + oracleHome, "ORACLE_SID=" + oracleSID, "LD_LIBRARY_PATH=" + filepath.Join(oracleHome, "lib"), "PATH=" + filepath.Join(oracleHome, "bin") + ":/usr/bin:/bin"}, + Timeout: 3600, // 1 hour timeout + }) + + logger.Infow("Datapatch execution result", "stdout", datapatchRes.StdOut, "stderr", datapatchRes.StdErr, "exit_code", datapatchRes.ExitCode) + + if datapatchRes.ExitCode != 0 { + return fmt.Errorf("datapatch failed with exit code %d: %s", datapatchRes.ExitCode, datapatchRes.StdErr) + } + + // Recompile invalid objects (utlrp.sql). + logger.Info("Executing utlrp.sql...") + utlrpStdout, utlrpStderr, utlrpErr := runSQL(ctx, params, "@?/rdbms/admin/utlrp", 3600, true) + if utlrpErr != nil { + return fmt.Errorf("utlrp execution failed: %w", utlrpErr) + } + logger.Infow("utlrp execution result", "stdout", utlrpStdout, "stderr", utlrpStderr) + + // Log patch registry for debugging. + regStdout, regStderr, regErr := runSQL(ctx, params, "SELECT action_time, action, status, patch_id FROM dba_registry_sqlpatch ORDER BY action_time;", 60, false) + if regErr != nil { + logger.Warnw("Failed to query dba_registry_sqlpatch", "stdout", regStdout, "stderr", regStderr, "error", regErr) + } + logger.Infow("Patch registry status", "stdout", regStdout) + + return nil } // DisableRestrictedSession implements the oracle_disable_restricted_mode guest action. diff --git a/internal/oraclehandlers/patching_test.go b/internal/oraclehandlers/patching_test.go index 02621d2..17b87a8 100644 --- a/internal/oraclehandlers/patching_test.go +++ b/internal/oraclehandlers/patching_test.go @@ -703,20 +703,111 @@ func TestDetectStartupMechanism(t *testing.T) { } } -func TestRunDatapatch_NotImplemented(t *testing.T) { - command := &gpb.Command{ - CommandType: &gpb.Command_AgentCommand{ - AgentCommand: &gpb.AgentCommand{ - Command: "oracle_run_datapatch", +func TestRunDatapatch(t *testing.T) { + defaultParams := map[string]string{ + "oracle_sid": "ORCL", + "oracle_home": "/u01/app/oracle/product/19.0.0/dbhome_1", + "oracle_user": "oracle", + } + + tests := []struct { + name string + params map[string]string + mockSQL map[string]*commandlineexecutor.Result // Mocks for runSQL + mockCmds map[string]commandlineexecutor.Result // Mocks for executeCommand (datapatch) + wantErrorCode codepb.Code + }{ + { + name: "Success", + params: defaultParams, + mockSQL: map[string]*commandlineexecutor.Result{ + "ALTER PLUGGABLE DATABASE ALL OPEN;": {ExitCode: 0, StdOut: "Pluggable database altered."}, + "@?/rdbms/admin/utlrp": {ExitCode: 0, StdOut: "PL/SQL procedure successfully completed."}, + "SELECT action_time, action, status, patch_id FROM dba_registry_sqlpatch ORDER BY action_time;": {ExitCode: 0, StdOut: "PATCH INFO"}, + }, + mockCmds: map[string]commandlineexecutor.Result{ + "datapatch": {ExitCode: 0, StdOut: "Datapatch successful"}, }, + wantErrorCode: codepb.Code_OK, + }, + { + name: "PDBOpenFailProceeds", + params: defaultParams, + mockSQL: map[string]*commandlineexecutor.Result{ + "ALTER PLUGGABLE DATABASE ALL OPEN;": {ExitCode: 1, Error: fmt.Errorf("ORA-65000: missing or invalid pluggable database name")}, + "@?/rdbms/admin/utlrp": {ExitCode: 0, StdOut: "PL/SQL procedure successfully completed."}, + "SELECT action_time, action, status, patch_id FROM dba_registry_sqlpatch ORDER BY action_time;": {ExitCode: 0, StdOut: "PATCH INFO"}, + }, + mockCmds: map[string]commandlineexecutor.Result{ + "datapatch": {ExitCode: 0, StdOut: "Datapatch successful"}, + }, + wantErrorCode: codepb.Code_OK, + }, + { + name: "DatapatchFail", + params: defaultParams, + mockSQL: map[string]*commandlineexecutor.Result{ + "ALTER PLUGGABLE DATABASE ALL OPEN;": {ExitCode: 0}, + }, + mockCmds: map[string]commandlineexecutor.Result{ + "datapatch": {ExitCode: 1, StdErr: "Datapatch failed"}, + }, + wantErrorCode: codepb.Code_INTERNAL, + }, + { + name: "UtlrpFail", + params: defaultParams, + mockSQL: map[string]*commandlineexecutor.Result{ + "ALTER PLUGGABLE DATABASE ALL OPEN;": {ExitCode: 0}, + "@?/rdbms/admin/utlrp": {ExitCode: 1, Error: fmt.Errorf("utlrp failed")}, + }, + mockCmds: map[string]commandlineexecutor.Result{ + "datapatch": {ExitCode: 0}, + }, + wantErrorCode: codepb.Code_INTERNAL, + }, + { + name: "ValidationFailMissingParams", + params: map[string]string{}, // Missing params + wantErrorCode: codepb.Code_INVALID_ARGUMENT, }, } - result := RunDatapatch(context.Background(), command, nil) - if result.GetExitCode() != 1 { - t.Errorf("RunDatapatch() returned exit code %d, want 1", result.GetExitCode()) - } - if !strings.Contains(result.GetStdout(), "not implemented") { - t.Errorf("RunDatapatch() returned stdout %q, want 'not implemented'", result.GetStdout()) + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // Mock runSQL + origRunSQL := runSQL + defer func() { runSQL = origRunSQL }() + runSQL = createMockRunSQL(tc.mockSQL) + + // Mock executeCommand for datapatch + origExecuteCommand := executeCommand + defer func() { executeCommand = origExecuteCommand }() + executeCommand = func(ctx context.Context, params commandlineexecutor.Params) commandlineexecutor.Result { + if strings.Contains(params.Executable, "datapatch") { + return tc.mockCmds["datapatch"] + } + return commandlineexecutor.Result{ExitCode: 1, StdErr: "Unknown command: " + params.Executable} + } + + command := &gpb.Command{ + CommandType: &gpb.Command_AgentCommand{ + AgentCommand: &gpb.AgentCommand{ + Command: "oracle_run_datapatch", + Parameters: tc.params, + }, + }, + } + result := RunDatapatch(context.Background(), command, nil) + + s := &spb.Status{} + if err := anypb.UnmarshalTo(result.Payload, s, proto.UnmarshalOptions{}); err != nil { + t.Fatalf("Failed to unmarshal payload: %v", err) + } + if s.Code != int32(tc.wantErrorCode) { + t.Errorf("RunDatapatch() with params %v returned error code %d, want %d", tc.params, s.Code, tc.wantErrorCode) + } + }) } }