mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-06 20:02:41 +08:00
feat(saphana): refactor backup from SQL export to BACKUP DATA USING FILE
Replace the hdbsql SELECT-based schema DDL export with SAP HANA's official BACKUP DATA USING FILE for proper data-level backup. Changes: - Run: issue BACKUP DATA [FOR <tenant>] USING FILE via hdbsql, package resulting backup files into tar archive as artifact - Restore: extract tar, locate backup prefix, issue RECOVER DATA [FOR <tenant>] USING FILE ... CLEAR LOG - Add helper functions: buildHdbsqlArgs, packageBackupFiles, extractTarArchive, findBackupPrefix - Add 7 unit tests covering backup/restore/error paths
This commit is contained in:
@@ -1,16 +1,20 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SAPHANARunner implements the BackupRunner interface for SAP HANA databases.
|
||||
// It uses the hdbsql CLI tool to execute SQL-based backup/restore operations.
|
||||
// It uses hdbsql to issue BACKUP DATA USING FILE commands for proper data-level
|
||||
// backup (SAP best practice), rather than logical SQL export.
|
||||
type SAPHANARunner struct {
|
||||
executor CommandExecutor
|
||||
}
|
||||
@@ -28,24 +32,30 @@ func (r *SAPHANARunner) Type() string {
|
||||
return "saphana"
|
||||
}
|
||||
|
||||
// Run executes a SAP HANA backup using hdbsql.
|
||||
// It connects to the HANA instance and triggers a BACKUP DATA command,
|
||||
// then packages the resulting backup files into a tar.gz archive.
|
||||
// Run executes a SAP HANA data-level backup using hdbsql + BACKUP DATA USING FILE.
|
||||
// The backup files are written to a temporary directory, then packaged into a tar
|
||||
// archive as the artifact for BackupX to compress/encrypt/upload.
|
||||
func (r *SAPHANARunner) Run(ctx context.Context, task TaskSpec, writer LogWriter) (*RunResult, error) {
|
||||
if _, err := r.executor.LookPath("hdbsql"); err != nil {
|
||||
return nil, fmt.Errorf("未找到 hdbsql 命令 (请确保服务器已安装 SAP HANA Client)")
|
||||
}
|
||||
|
||||
tempDir, artifactPath, err := createTempArtifact(task.TempDir, task.Name, "sql")
|
||||
startedAt := task.StartedAt
|
||||
if startedAt.IsZero() {
|
||||
startedAt = time.Now().UTC()
|
||||
}
|
||||
|
||||
// Create a temp directory for the tar artifact output.
|
||||
tempDir, artifactPath, err := createTempArtifact(task.TempDir, task.Name, "tar")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file, err := os.Create(artifactPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create SAP HANA dump file: %w", err)
|
||||
// Create a sub-directory where HANA will write its backup data files.
|
||||
backupDir := filepath.Join(tempDir, "hana_data")
|
||||
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("create HANA backup directory: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
dbNames := normalizeDatabaseNames(task.Database.Names)
|
||||
tenantDB := "SYSTEMDB"
|
||||
@@ -61,78 +71,56 @@ func (r *SAPHANARunner) Run(ctx context.Context, task TaskSpec, writer LogWriter
|
||||
writer.WriteLine(fmt.Sprintf("连接到 SAP HANA: %s:%d", task.Database.Host, port))
|
||||
writer.WriteLine(fmt.Sprintf("备份数据库: %s", tenantDB))
|
||||
|
||||
// Build hdbsql connection arguments
|
||||
args := []string{
|
||||
"-n", fmt.Sprintf("%s:%d", task.Database.Host, port),
|
||||
"-u", task.Database.User,
|
||||
"-p", task.Database.Password,
|
||||
"-d", tenantDB,
|
||||
"-j", // disable auto-commit
|
||||
"-A", // disable column alignment
|
||||
"-xC", // suppress column headers and separator
|
||||
// Build backup prefix — HANA will create files like <prefix>_databackup_<N>_1.
|
||||
timestamp := startedAt.UTC().Format("20060102_150405")
|
||||
backupPrefix := filepath.Join(backupDir, fmt.Sprintf("hana_%s_%s", strings.ToLower(tenantDB), timestamp))
|
||||
|
||||
// Build `BACKUP DATA USING FILE` SQL.
|
||||
backupSQL := fmt.Sprintf(`BACKUP DATA USING FILE ('%s')`, backupPrefix)
|
||||
if strings.ToUpper(tenantDB) != "SYSTEMDB" {
|
||||
backupSQL = fmt.Sprintf(`BACKUP DATA FOR %s USING FILE ('%s')`, tenantDB, backupPrefix)
|
||||
}
|
||||
|
||||
// Export schema using SELECT statements for each table.
|
||||
// We use hdbsql to query system catalog and dump table data as SQL INSERT statements.
|
||||
exportSQL := fmt.Sprintf(`SELECT
|
||||
'CREATE SCHEMA "' || SCHEMA_NAME || '";'
|
||||
FROM SCHEMAS
|
||||
WHERE HAS_PRIVILEGES = 'TRUE'
|
||||
AND SCHEMA_NAME NOT LIKE '%%SYS%%'
|
||||
AND SCHEMA_NAME NOT LIKE '_%%'
|
||||
AND SCHEMA_NAME != 'SAP_REST_API'
|
||||
ORDER BY SCHEMA_NAME`)
|
||||
|
||||
exportArgs := append(append([]string{}, args...), exportSQL)
|
||||
// Construct hdbsql connection arguments.
|
||||
args := buildHdbsqlArgs(task.Database.Host, port, task.Database.User, task.Database.Password, tenantDB, backupSQL)
|
||||
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
writer.WriteLine("开始执行 SAP HANA 数据导出")
|
||||
writer.WriteLine("开始执行 SAP HANA BACKUP DATA USING FILE")
|
||||
|
||||
if err := r.executor.Run(ctx, "hdbsql", exportArgs, CommandOptions{
|
||||
Stdout: file,
|
||||
if err := r.executor.Run(ctx, "hdbsql", args, CommandOptions{
|
||||
Stderr: stderrWriter,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("run hdbsql export: %w: %s", err, stderrWriter.collected())
|
||||
return nil, fmt.Errorf("run hdbsql BACKUP DATA: %w: %s", err, stderrWriter.collected())
|
||||
}
|
||||
|
||||
// If multiple databases were specified, export each additional one
|
||||
for i := 1; i < len(dbNames); i++ {
|
||||
writer.WriteLine(fmt.Sprintf("导出额外数据库: %s", dbNames[i]))
|
||||
if _, writeErr := file.WriteString(fmt.Sprintf("\n-- Database: %s\n", dbNames[i])); writeErr != nil {
|
||||
return nil, fmt.Errorf("write database separator: %w", writeErr)
|
||||
}
|
||||
writer.WriteLine("SAP HANA BACKUP DATA 命令执行完成,开始打包备份文件")
|
||||
|
||||
additionalArgs := []string{
|
||||
"-n", fmt.Sprintf("%s:%d", task.Database.Host, port),
|
||||
"-u", task.Database.User,
|
||||
"-p", task.Database.Password,
|
||||
"-d", dbNames[i],
|
||||
"-j", "-A", "-xC",
|
||||
exportSQL,
|
||||
}
|
||||
if err := r.executor.Run(ctx, "hdbsql", additionalArgs, CommandOptions{
|
||||
Stdout: file,
|
||||
Stderr: stderrWriter,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("run hdbsql export for %s: %w", dbNames[i], err)
|
||||
}
|
||||
// Package all generated backup files into a tar archive.
|
||||
if err := packageBackupFiles(backupDir, artifactPath, writer); err != nil {
|
||||
return nil, fmt.Errorf("package HANA backup files: %w", err)
|
||||
}
|
||||
|
||||
info, _ := file.Stat()
|
||||
info, _ := os.Stat(artifactPath)
|
||||
sizeStr := "未知"
|
||||
var fileSize int64
|
||||
if info != nil {
|
||||
sizeStr = formatFileSize(info.Size())
|
||||
fileSize = info.Size()
|
||||
sizeStr = formatFileSize(fileSize)
|
||||
}
|
||||
writer.WriteLine(fmt.Sprintf("SAP HANA 导出完成(文件大小: %s)", sizeStr))
|
||||
writer.WriteLine(fmt.Sprintf("SAP HANA 备份完成(归档大小: %s)", sizeStr))
|
||||
|
||||
return &RunResult{
|
||||
ArtifactPath: artifactPath,
|
||||
FileName: filepath.Base(artifactPath),
|
||||
TempDir: tempDir,
|
||||
Size: fileSize,
|
||||
StorageKey: BuildStorageKey("saphana", startedAt, filepath.Base(artifactPath)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Restore executes a SAP HANA restore using hdbsql to replay the SQL dump file.
|
||||
// Restore executes a SAP HANA restore using RECOVER DATA USING FILE.
|
||||
// It extracts the tar archive to get the original backup files, then issues
|
||||
// the recovery SQL command via hdbsql.
|
||||
func (r *SAPHANARunner) Restore(ctx context.Context, task TaskSpec, artifactPath string, writer LogWriter) error {
|
||||
if _, err := r.executor.LookPath("hdbsql"); err != nil {
|
||||
return fmt.Errorf("未找到 hdbsql 命令 (请确保服务器已安装 SAP HANA Client)")
|
||||
@@ -151,27 +139,39 @@ func (r *SAPHANARunner) Restore(ctx context.Context, task TaskSpec, artifactPath
|
||||
|
||||
writer.WriteLine(fmt.Sprintf("开始恢复 SAP HANA 数据库: %s", tenantDB))
|
||||
|
||||
input, err := os.Open(filepath.Clean(artifactPath))
|
||||
// Extract the tar archive to a temporary directory.
|
||||
restoreDir, err := os.MkdirTemp("", "backupx-hana-restore-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("open SAP HANA restore file: %w", err)
|
||||
return fmt.Errorf("create restore temp dir: %w", err)
|
||||
}
|
||||
defer input.Close()
|
||||
defer os.RemoveAll(restoreDir)
|
||||
|
||||
args := []string{
|
||||
"-n", fmt.Sprintf("%s:%d", task.Database.Host, port),
|
||||
"-u", task.Database.User,
|
||||
"-p", task.Database.Password,
|
||||
"-d", tenantDB,
|
||||
"-j",
|
||||
"-I", artifactPath,
|
||||
if err := extractTarArchive(artifactPath, restoreDir); err != nil {
|
||||
return fmt.Errorf("extract HANA backup tar: %w", err)
|
||||
}
|
||||
|
||||
// Find the backup prefix by locating backup data files.
|
||||
prefix, err := findBackupPrefix(restoreDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find backup prefix: %w", err)
|
||||
}
|
||||
|
||||
writer.WriteLine(fmt.Sprintf("找到备份前缀: %s", filepath.Base(prefix)))
|
||||
|
||||
// Build RECOVER DATA SQL.
|
||||
recoverSQL := fmt.Sprintf(`RECOVER DATA USING FILE ('%s') CLEAR LOG`, prefix)
|
||||
if strings.ToUpper(tenantDB) != "SYSTEMDB" {
|
||||
recoverSQL = fmt.Sprintf(`RECOVER DATA FOR %s USING FILE ('%s') CLEAR LOG`, tenantDB, prefix)
|
||||
}
|
||||
|
||||
args := buildHdbsqlArgs(task.Database.Host, port, task.Database.User, task.Database.Password, tenantDB, recoverSQL)
|
||||
|
||||
stderrWriter := newLogLineWriter(writer, "hdbsql")
|
||||
if err := r.executor.Run(ctx, "hdbsql", args, CommandOptions{
|
||||
Stderr: stderrWriter,
|
||||
}); err != nil {
|
||||
errMsg := stderrWriter.collected()
|
||||
return fmt.Errorf("run hdbsql restore: %w: %s", err, strings.TrimSpace(errMsg))
|
||||
return fmt.Errorf("run hdbsql RECOVER DATA: %w: %s", err, strings.TrimSpace(errMsg))
|
||||
}
|
||||
|
||||
writer.WriteLine("SAP HANA 恢复完成")
|
||||
@@ -187,3 +187,153 @@ func hanaInstanceNumber(port int) string {
|
||||
}
|
||||
return "00"
|
||||
}
|
||||
|
||||
// buildHdbsqlArgs constructs the common hdbsql CLI arguments.
|
||||
func buildHdbsqlArgs(host string, port int, user, password, database, sql string) []string {
|
||||
return []string{
|
||||
"-n", fmt.Sprintf("%s:%d", host, port),
|
||||
"-u", user,
|
||||
"-p", password,
|
||||
"-d", database,
|
||||
"-j", // disable auto-commit
|
||||
"-A", // disable column alignment
|
||||
"-xC", // suppress column headers and separator
|
||||
sql,
|
||||
}
|
||||
}
|
||||
|
||||
// packageBackupFiles creates a tar archive from all files in the given directory.
|
||||
func packageBackupFiles(sourceDir, targetPath string, writer LogWriter) error {
|
||||
file, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create tar file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
tw := tar.NewWriter(file)
|
||||
defer tw.Close()
|
||||
|
||||
fileCount := 0
|
||||
walkErr := filepath.Walk(sourceDir, func(currentPath string, info os.FileInfo, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if currentPath == sourceDir {
|
||||
return nil
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(sourceDir, currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filepath.ToSlash(relPath)
|
||||
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode().IsRegular() {
|
||||
f, err := os.Open(currentPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
if _, err := io.CopyN(tw, f, info.Size()); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
fileCount++
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
|
||||
if fileCount == 0 {
|
||||
return fmt.Errorf("HANA 备份目录中未找到任何备份文件")
|
||||
}
|
||||
|
||||
writer.WriteLine(fmt.Sprintf("已打包 %d 个备份文件", fileCount))
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractTarArchive extracts a tar archive to the given directory.
|
||||
func extractTarArchive(tarPath, targetDir string) error {
|
||||
f, err := os.Open(filepath.Clean(tarPath))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
tr := tar.NewReader(f)
|
||||
for {
|
||||
header, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read tar entry: %w", err)
|
||||
}
|
||||
|
||||
targetPath := filepath.Join(targetDir, filepath.FromSlash(filepath.Clean(header.Name)))
|
||||
// Guard against path traversal.
|
||||
if !strings.HasPrefix(targetPath, filepath.Clean(targetDir)+string(filepath.Separator)) {
|
||||
continue
|
||||
}
|
||||
|
||||
switch header.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err := os.MkdirAll(targetPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
case tar.TypeReg, tar.TypeRegA:
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
outFile, err := os.Create(targetPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(outFile, tr); err != nil {
|
||||
outFile.Close()
|
||||
return err
|
||||
}
|
||||
outFile.Close()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// findBackupPrefix locates the backup prefix by scanning for HANA backup data files.
|
||||
// HANA creates files like <prefix>_databackup_0_1, <prefix>_databackup_1_1, etc.
|
||||
func findBackupPrefix(dir string) (string, error) {
|
||||
var prefix string
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil || info.IsDir() {
|
||||
return err
|
||||
}
|
||||
name := info.Name()
|
||||
if idx := strings.Index(name, "_databackup_"); idx > 0 {
|
||||
prefix = filepath.Join(filepath.Dir(path), name[:idx])
|
||||
return filepath.SkipAll
|
||||
}
|
||||
// Also check for the complete backup file pattern without _databackup_
|
||||
if strings.HasPrefix(name, "hana_") {
|
||||
prefix = filepath.Join(filepath.Dir(path), strings.TrimSuffix(name, filepath.Ext(name)))
|
||||
return filepath.SkipAll
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil && err != filepath.SkipAll {
|
||||
return "", err
|
||||
}
|
||||
if prefix == "" {
|
||||
return "", fmt.Errorf("未在归档中找到 HANA 备份数据文件")
|
||||
}
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
294
server/internal/backup/saphana_runner_test.go
Normal file
294
server/internal/backup/saphana_runner_test.go
Normal file
@@ -0,0 +1,294 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSAPHANARunnerRun_BackupDataCommand(t *testing.T) {
|
||||
var capturedArgs []string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedArgs = append([]string{}, args...)
|
||||
// Simulate HANA creating backup data files in the directory from the SQL.
|
||||
// Parse the backup prefix from the SQL argument (last arg).
|
||||
sql := args[len(args)-1]
|
||||
// Extract path from: BACKUP DATA USING FILE ('/path/to/hana_systemdb_...')
|
||||
startIdx := strings.Index(sql, "('") + 2
|
||||
endIdx := strings.Index(sql, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := sql[startIdx:endIdx]
|
||||
dir := filepath.Dir(prefix)
|
||||
_ = os.MkdirAll(dir, 0o755)
|
||||
// Create fake backup data files that HANA would produce.
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("fake backup data volume 0"), 0o644)
|
||||
_ = os.WriteFile(prefix+"_databackup_1_1", []byte("fake backup data volume 1"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-daily",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1",
|
||||
Port: 30015,
|
||||
User: "SYSTEM",
|
||||
Password: "secret",
|
||||
Names: []string{"SYSTEMDB"},
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
|
||||
// Verify hdbsql was called with the correct connection args.
|
||||
if len(capturedArgs) == 0 {
|
||||
t.Fatal("expected hdbsql args to be captured")
|
||||
}
|
||||
|
||||
// Check host:port
|
||||
foundHost := false
|
||||
for i, arg := range capturedArgs {
|
||||
if arg == "-n" && i+1 < len(capturedArgs) && capturedArgs[i+1] == "10.0.0.1:30015" {
|
||||
foundHost = true
|
||||
}
|
||||
}
|
||||
if !foundHost {
|
||||
t.Fatalf("expected host:port 10.0.0.1:30015 in args, got: %v", capturedArgs)
|
||||
}
|
||||
|
||||
// Verify the SQL contains BACKUP DATA USING FILE.
|
||||
lastArg := capturedArgs[len(capturedArgs)-1]
|
||||
if !strings.Contains(lastArg, "BACKUP DATA USING FILE") {
|
||||
t.Fatalf("expected BACKUP DATA USING FILE in SQL, got: %s", lastArg)
|
||||
}
|
||||
|
||||
// Verify artifact is a tar file.
|
||||
if !strings.HasSuffix(result.ArtifactPath, ".tar") {
|
||||
t.Fatalf("expected .tar artifact, got: %s", result.ArtifactPath)
|
||||
}
|
||||
|
||||
// Verify artifact file exists and has content.
|
||||
info, err := os.Stat(result.ArtifactPath)
|
||||
if err != nil {
|
||||
t.Fatalf("artifact file missing: %v", err)
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
t.Fatal("artifact tar file is empty")
|
||||
}
|
||||
|
||||
// Cleanup.
|
||||
os.RemoveAll(result.TempDir)
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_TenantDatabase(t *testing.T) {
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
// Simulate HANA creating backup files.
|
||||
startIdx := strings.Index(capturedSQL, "('") + 2
|
||||
endIdx := strings.Index(capturedSQL, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := capturedSQL[startIdx:endIdx]
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("data"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-tenant",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1",
|
||||
Port: 30015,
|
||||
User: "SYSTEM",
|
||||
Password: "secret",
|
||||
Names: []string{"HDB"},
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
// For tenant databases, the SQL should use BACKUP DATA FOR <tenant>.
|
||||
if !strings.Contains(capturedSQL, "BACKUP DATA FOR HDB USING FILE") {
|
||||
t.Fatalf("expected BACKUP DATA FOR HDB in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_DefaultPort(t *testing.T) {
|
||||
var capturedArgs []string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedArgs = append([]string{}, args...)
|
||||
sql := args[len(args)-1]
|
||||
startIdx := strings.Index(sql, "('") + 2
|
||||
endIdx := strings.Index(sql, "')")
|
||||
if startIdx > 1 && endIdx > startIdx {
|
||||
prefix := sql[startIdx:endIdx]
|
||||
_ = os.MkdirAll(filepath.Dir(prefix), 0o755)
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("data"), 0o644)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
result, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-default-port",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "localhost",
|
||||
Port: 0, // Should default to 30015
|
||||
User: "SYSTEM",
|
||||
Password: "secret",
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Run returned error: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(result.TempDir)
|
||||
|
||||
// Verify default port 30015 was used.
|
||||
for i, arg := range capturedArgs {
|
||||
if arg == "-n" && i+1 < len(capturedArgs) {
|
||||
if !strings.HasSuffix(capturedArgs[i+1], ":30015") {
|
||||
t.Fatalf("expected default port 30015, got: %s", capturedArgs[i+1])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRun_LookPathError(t *testing.T) {
|
||||
runner := NewSAPHANARunner(&fakeCommandExecutor{lookupErr: errors.New("not found")})
|
||||
_, err := runner.Run(context.Background(), TaskSpec{
|
||||
Name: "hana-missing",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
},
|
||||
}, NopLogWriter{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error when hdbsql is missing")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "hdbsql") {
|
||||
t.Fatalf("error should mention hdbsql, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRestore_RecoverDataCommand(t *testing.T) {
|
||||
// First, create a fake tar archive with a backup data file.
|
||||
tarDir := t.TempDir()
|
||||
dataDir := filepath.Join(tarDir, "hana_data")
|
||||
_ = os.MkdirAll(dataDir, 0o755)
|
||||
prefix := filepath.Join(dataDir, "hana_systemdb_20260324_120000")
|
||||
_ = os.WriteFile(prefix+"_databackup_0_1", []byte("backup data"), 0o644)
|
||||
|
||||
// Create the tar.
|
||||
tarPath := filepath.Join(tarDir, "backup.tar")
|
||||
if err := packageBackupFiles(dataDir, tarPath, NopLogWriter{}); err != nil {
|
||||
t.Fatalf("failed to create test tar: %v", err)
|
||||
}
|
||||
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
err := runner.Restore(context.Background(), TaskSpec{
|
||||
Name: "hana-restore",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
Names: []string{"SYSTEMDB"},
|
||||
},
|
||||
}, tarPath, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Restore returned error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(capturedSQL, "RECOVER DATA USING FILE") {
|
||||
t.Fatalf("expected RECOVER DATA USING FILE in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
if !strings.Contains(capturedSQL, "CLEAR LOG") {
|
||||
t.Fatalf("expected CLEAR LOG in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSAPHANARunnerRestore_TenantRecoverCommand(t *testing.T) {
|
||||
tarDir := t.TempDir()
|
||||
dataDir := filepath.Join(tarDir, "data")
|
||||
_ = os.MkdirAll(dataDir, 0o755)
|
||||
_ = os.WriteFile(filepath.Join(dataDir, "hana_hdb_20260324_120000_databackup_0_1"), []byte("data"), 0o644)
|
||||
|
||||
tarPath := filepath.Join(tarDir, "backup.tar")
|
||||
if err := packageBackupFiles(dataDir, tarPath, NopLogWriter{}); err != nil {
|
||||
t.Fatalf("failed to create test tar: %v", err)
|
||||
}
|
||||
|
||||
var capturedSQL string
|
||||
executor := &fakeCommandExecutor{
|
||||
runFunc: func(name string, args []string, options CommandOptions) error {
|
||||
capturedSQL = args[len(args)-1]
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
runner := NewSAPHANARunner(executor)
|
||||
err := runner.Restore(context.Background(), TaskSpec{
|
||||
Name: "hana-tenant-restore",
|
||||
Type: "saphana",
|
||||
Database: DatabaseSpec{
|
||||
Host: "10.0.0.1", Port: 30015, User: "SYSTEM", Password: "secret",
|
||||
Names: []string{"HDB"},
|
||||
},
|
||||
}, tarPath, NopLogWriter{})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Restore returned error: %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(capturedSQL, "RECOVER DATA FOR HDB USING FILE") {
|
||||
t.Fatalf("expected RECOVER DATA FOR HDB in SQL, got: %s", capturedSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHanaInstanceNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
port int
|
||||
expected string
|
||||
}{
|
||||
{30015, "0"},
|
||||
{30115, "1"},
|
||||
{30215, "2"},
|
||||
{31015, "10"},
|
||||
{25000, "00"},
|
||||
{40001, "00"},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := hanaInstanceNumber(tc.port)
|
||||
if got != tc.expected {
|
||||
t.Errorf("hanaInstanceNumber(%d) = %s, want %s", tc.port, got, tc.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user