github.com/percona/percona-xtradb-cluster-operator@v1.14.0/cmd/pitr/recoverer/recoverer.go (about) 1 package recoverer 2 3 import ( 4 "bytes" 5 "context" 6 "io" 7 "log" 8 "net/url" 9 "os" 10 "os/exec" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/percona/percona-xtradb-cluster-operator/cmd/pitr/pxc" 17 "github.com/percona/percona-xtradb-cluster-operator/pkg/pxc/backup/storage" 18 19 "github.com/pkg/errors" 20 ) 21 22 type Recoverer struct { 23 db *pxc.PXC 24 recoverTime string 25 storage storage.Storage 26 pxcUser string 27 pxcPass string 28 recoverType RecoverType 29 pxcServiceName string 30 binlogs []string 31 gtidSet string 32 startGTID string 33 recoverFlag string 34 recoverEndTime time.Time 35 gtid string 36 verifyTLS bool 37 } 38 39 type Config struct { 40 PXCServiceName string `env:"PXC_SERVICE,required"` 41 PXCUser string `env:"PXC_USER,required"` 42 PXCPass string `env:"PXC_PASS,required"` 43 BackupStorageS3 BackupS3 44 BackupStorageAzure BackupAzure 45 RecoverTime string `env:"PITR_DATE"` 46 RecoverType string `env:"PITR_RECOVERY_TYPE,required"` 47 GTID string `env:"PITR_GTID"` 48 VerifyTLS bool `env:"VERIFY_TLS" envDefault:"true"` 49 StorageType string `env:"STORAGE_TYPE,required"` 50 BinlogStorageS3 BinlogS3 51 BinlogStorageAzure BinlogAzure 52 } 53 54 func (c Config) storages(ctx context.Context) (storage.Storage, storage.Storage, error) { 55 var binlogStorage, defaultStorage storage.Storage 56 switch c.StorageType { 57 case "s3": 58 bucket, prefix, err := getBucketAndPrefix(c.BinlogStorageS3.BucketURL) 59 if err != nil { 60 return nil, nil, errors.Wrap(err, "get bucket and prefix") 61 } 62 binlogStorage, err = storage.NewS3(ctx, c.BinlogStorageS3.Endpoint, c.BinlogStorageS3.AccessKeyID, c.BinlogStorageS3.AccessKey, bucket, prefix, c.BinlogStorageS3.Region, c.VerifyTLS) 63 if err != nil { 64 return nil, nil, errors.Wrap(err, "new s3 storage") 65 } 66 67 bucket, prefix, err = getBucketAndPrefix(c.BackupStorageS3.BackupDest) 68 if err != nil { 69 return nil, nil, errors.Wrap(err, "get bucket and prefix") 70 } 71 prefix = prefix[:len(prefix)-1] 72 defaultStorage, err = storage.NewS3(ctx, c.BackupStorageS3.Endpoint, c.BackupStorageS3.AccessKeyID, c.BackupStorageS3.AccessKey, bucket, prefix+".sst_info/", c.BackupStorageS3.Region, c.VerifyTLS) 73 if err != nil { 74 return nil, nil, errors.Wrap(err, "new storage manager") 75 } 76 case "azure": 77 var err error 78 container, prefix := getContainerAndPrefix(c.BinlogStorageAzure.ContainerPath) 79 binlogStorage, err = storage.NewAzure(c.BinlogStorageAzure.AccountName, c.BinlogStorageAzure.AccountKey, c.BinlogStorageAzure.Endpoint, container, prefix) 80 if err != nil { 81 return nil, nil, errors.Wrap(err, "new azure storage") 82 } 83 defaultStorage, err = storage.NewAzure(c.BackupStorageAzure.AccountName, c.BackupStorageAzure.AccountKey, c.BackupStorageAzure.Endpoint, c.BackupStorageAzure.ContainerName, c.BackupStorageAzure.BackupDest+".sst_info/") 84 if err != nil { 85 return nil, nil, errors.Wrap(err, "new azure storage") 86 } 87 default: 88 return nil, nil, errors.New("unknown STORAGE_TYPE") 89 } 90 return binlogStorage, defaultStorage, nil 91 } 92 93 type BackupS3 struct { 94 Endpoint string `env:"ENDPOINT" envDefault:"s3.amazonaws.com"` 95 AccessKeyID string `env:"ACCESS_KEY_ID,required"` 96 AccessKey string `env:"SECRET_ACCESS_KEY,required"` 97 Region string `env:"DEFAULT_REGION,required"` 98 BackupDest string `env:"S3_BUCKET_URL,required"` 99 } 100 101 type BackupAzure struct { 102 Endpoint string `env:"AZURE_ENDPOINT,required"` 103 ContainerName string `env:"AZURE_CONTAINER_NAME,required"` 104 StorageClass string `env:"AZURE_STORAGE_CLASS"` 105 AccountName string `env:"AZURE_STORAGE_ACCOUNT,required"` 106 AccountKey string `env:"AZURE_ACCESS_KEY,required"` 107 BackupDest string `env:"BACKUP_PATH,required"` 108 } 109 110 type BinlogS3 struct { 111 Endpoint string `env:"BINLOG_S3_ENDPOINT" envDefault:"s3.amazonaws.com"` 112 AccessKeyID string `env:"BINLOG_ACCESS_KEY_ID,required"` 113 AccessKey string `env:"BINLOG_SECRET_ACCESS_KEY,required"` 114 Region string `env:"BINLOG_S3_REGION,required"` 115 BucketURL string `env:"BINLOG_S3_BUCKET_URL,required"` 116 } 117 118 type BinlogAzure struct { 119 Endpoint string `env:"BINLOG_AZURE_ENDPOINT,required"` 120 ContainerPath string `env:"BINLOG_AZURE_CONTAINER_PATH,required"` 121 StorageClass string `env:"BINLOG_AZURE_STORAGE_CLASS"` 122 AccountName string `env:"BINLOG_AZURE_STORAGE_ACCOUNT,required"` 123 AccountKey string `env:"BINLOG_AZURE_ACCESS_KEY,required"` 124 } 125 126 func (c *Config) Verify() { 127 if len(c.BackupStorageS3.Endpoint) == 0 { 128 c.BackupStorageS3.Endpoint = "s3.amazonaws.com" 129 } 130 if len(c.BinlogStorageS3.Endpoint) == 0 { 131 c.BinlogStorageS3.Endpoint = "s3.amazonaws.com" 132 } 133 } 134 135 type RecoverType string 136 137 func New(ctx context.Context, c Config) (*Recoverer, error) { 138 c.Verify() 139 140 binlogStorage, storage, err := c.storages(ctx) 141 if err != nil { 142 return nil, errors.Wrap(err, "new binlog storage manager") 143 } 144 145 startGTID, err := getStartGTIDSet(ctx, storage) 146 if err != nil { 147 return nil, errors.Wrap(err, "get start GTID") 148 } 149 150 if c.RecoverType == string(Transaction) { 151 gtidSplitted := strings.Split(startGTID, ":") 152 if len(gtidSplitted) != 2 { 153 return nil, errors.New("Invalid start gtidset provided") 154 } 155 lastSetIdx := 1 156 setSplitted := strings.Split(gtidSplitted[1], "-") 157 if len(setSplitted) == 1 { 158 lastSetIdx = 0 159 } 160 lastSet := setSplitted[lastSetIdx] 161 lastSetInt, err := strconv.ParseInt(lastSet, 10, 64) 162 if err != nil { 163 return nil, errors.Wrap(err, "failed to cast last set value to in") 164 } 165 transactionNum, err := strconv.ParseInt(strings.Split(c.GTID, ":")[1], 10, 64) 166 if err != nil { 167 return nil, errors.Wrap(err, "failed to parse transaction num to restore") 168 } 169 if transactionNum < lastSetInt { 170 return nil, errors.New("Can't restore to transaction before backup") 171 } 172 } 173 174 return &Recoverer{ 175 storage: binlogStorage, 176 recoverTime: c.RecoverTime, 177 pxcUser: c.PXCUser, 178 pxcPass: c.PXCPass, 179 pxcServiceName: c.PXCServiceName, 180 recoverType: RecoverType(c.RecoverType), 181 startGTID: startGTID, 182 gtid: c.GTID, 183 verifyTLS: c.VerifyTLS, 184 }, nil 185 } 186 187 func getContainerAndPrefix(s string) (string, string) { 188 container, prefix, _ := strings.Cut(s, "/") 189 if prefix != "" { 190 prefix += "/" 191 } 192 return container, prefix 193 } 194 195 func getBucketAndPrefix(bucketURL string) (bucket string, prefix string, err error) { 196 u, err := url.Parse(bucketURL) 197 if err != nil { 198 err = errors.Wrap(err, "parse url") 199 return bucket, prefix, err 200 } 201 path := strings.TrimPrefix(strings.TrimSuffix(u.Path, "/"), "/") 202 203 if u.IsAbs() && u.Scheme == "s3" { 204 bucket = u.Host 205 prefix = path + "/" 206 return bucket, prefix, err 207 } 208 bucketArr := strings.Split(path, "/") 209 if len(bucketArr) > 1 { 210 prefix = strings.TrimPrefix(path, bucketArr[0]+"/") + "/" 211 } 212 bucket = bucketArr[0] 213 if len(bucket) == 0 { 214 err = errors.Errorf("can't get bucket name from %s", bucketURL) 215 return bucket, prefix, err 216 } 217 218 return bucket, prefix, err 219 } 220 221 func getStartGTIDSet(ctx context.Context, s storage.Storage) (string, error) { 222 sstInfo, err := s.ListObjects(ctx, "sst_info") 223 if err != nil { 224 return "", errors.Wrapf(err, "list objects") 225 } 226 if len(sstInfo) == 0 { 227 return "", errors.New("no info files in sst dir") 228 } 229 sort.Strings(sstInfo) 230 231 sstInfoObj, err := s.GetObject(ctx, sstInfo[0]) 232 if err != nil { 233 return "", errors.Wrapf(err, "get object") 234 } 235 defer sstInfoObj.Close() 236 237 s.SetPrefix(strings.TrimSuffix(s.GetPrefix(), ".sst_info/") + "/") 238 xtrabackupInfo, err := s.ListObjects(ctx, "xtrabackup_info") 239 if err != nil { 240 return "", errors.Wrapf(err, "list objects") 241 } 242 if len(xtrabackupInfo) == 0 { 243 return "", errors.New("no info files in backup") 244 } 245 sort.Strings(xtrabackupInfo) 246 247 xtrabackupInfoObj, err := s.GetObject(ctx, xtrabackupInfo[0]) 248 if err != nil { 249 return "", errors.Wrapf(err, "get object") 250 } 251 252 lastGTID, err := getLastBackupGTID(ctx, sstInfoObj, xtrabackupInfoObj) 253 if err != nil { 254 return "", errors.Wrap(err, "get last backup gtid") 255 } 256 257 return lastGTID, nil 258 } 259 260 const ( 261 Latest RecoverType = "latest" // recover to the latest existing binlog 262 Date RecoverType = "date" // recover to exact date 263 Transaction RecoverType = "transaction" // recover to needed trunsaction 264 Skip RecoverType = "skip" // skip transactions 265 ) 266 267 func (r *Recoverer) Run(ctx context.Context) error { 268 host, err := pxc.GetPXCFirstHost(ctx, r.pxcServiceName) 269 if err != nil { 270 return errors.Wrap(err, "get host") 271 } 272 r.db, err = pxc.NewPXC(host, r.pxcUser, r.pxcPass) 273 if err != nil { 274 return errors.Wrapf(err, "new manager with host %s", host) 275 } 276 277 err = r.setBinlogs(ctx) 278 if err != nil { 279 return errors.Wrap(err, "get binlog list") 280 } 281 282 switch r.recoverType { 283 case Skip: 284 r.recoverFlag = "--exclude-gtids=" + r.gtid 285 case Transaction: 286 r.recoverFlag = "--exclude-gtids=" + r.gtidSet 287 case Date: 288 r.recoverFlag = `--stop-datetime="` + r.recoverTime + `"` 289 290 const format = "2006-01-02 15:04:05" 291 endTime, err := time.Parse(format, r.recoverTime) 292 if err != nil { 293 return errors.Wrap(err, "parse date") 294 } 295 r.recoverEndTime = endTime 296 case Latest: 297 default: 298 return errors.New("wrong recover type") 299 } 300 301 err = r.recover(ctx) 302 if err != nil { 303 return errors.Wrap(err, "recover") 304 } 305 306 return nil 307 } 308 309 func (r *Recoverer) recover(ctx context.Context) (err error) { 310 err = r.db.DropCollectorFunctions(ctx) 311 if err != nil { 312 return errors.Wrap(err, "drop collector funcs") 313 } 314 315 err = os.Setenv("MYSQL_PWD", os.Getenv("PXC_PASS")) 316 if err != nil { 317 return errors.Wrap(err, "set mysql pwd env var") 318 } 319 320 mysqlStdin, binlogStdout := io.Pipe() 321 defer mysqlStdin.Close() 322 323 mysqlCmd := exec.CommandContext(ctx, "mysql", "-h", r.db.GetHost(), "-P", "33062", "-u", r.pxcUser) 324 log.Printf("Running %s", mysqlCmd.String()) 325 mysqlCmd.Stdin = mysqlStdin 326 mysqlCmd.Stderr = os.Stderr 327 mysqlCmd.Stdout = os.Stdout 328 if err := mysqlCmd.Start(); err != nil { 329 return errors.Wrap(err, "start mysql") 330 } 331 332 for i, binlog := range r.binlogs { 333 remaining := len(r.binlogs) - i 334 log.Printf("working with %s, %d out of %d remaining\n", binlog, remaining, len(r.binlogs)) 335 if r.recoverType == Date { 336 binlogArr := strings.Split(binlog, "_") 337 if len(binlogArr) < 2 { 338 return errors.New("get timestamp from binlog name") 339 } 340 binlogTime, err := strconv.ParseInt(binlogArr[1], 10, 64) 341 if err != nil { 342 return errors.Wrap(err, "get binlog time") 343 } 344 if binlogTime > r.recoverEndTime.Unix() { 345 log.Printf("Stopping at %s because it's after the recovery time (%d > %d)", binlog, binlogTime, r.recoverEndTime.Unix()) 346 break 347 } 348 } 349 350 binlogObj, err := r.storage.GetObject(ctx, binlog) 351 if err != nil { 352 return errors.Wrap(err, "get obj") 353 } 354 355 cmd := exec.CommandContext(ctx, "sh", "-c", "mysqlbinlog --disable-log-bin "+r.recoverFlag+" -") 356 log.Printf("Running %s", cmd.String()) 357 cmd.Stdin = binlogObj 358 cmd.Stdout = binlogStdout 359 cmd.Stderr = os.Stderr 360 err = cmd.Run() 361 if err != nil { 362 return errors.Wrapf(err, "run mysqlbinlog") 363 } 364 } 365 366 if err := binlogStdout.Close(); err != nil { 367 return errors.Wrap(err, "close binlog stdout") 368 } 369 370 log.Printf("Waiting for mysql to finish") 371 372 if err := mysqlCmd.Wait(); err != nil { 373 return errors.Wrap(err, "wait mysql") 374 } 375 376 log.Printf("Finished") 377 378 return nil 379 } 380 381 func getLastBackupGTID(ctx context.Context, sstInfo, xtrabackupInfo io.Reader) (string, error) { 382 sstContent, err := getDecompressedContent(ctx, sstInfo, "sst_info") 383 if err != nil { 384 return "", errors.Wrap(err, "get sst_info content") 385 } 386 387 xtrabackupContent, err := getDecompressedContent(ctx, xtrabackupInfo, "xtrabackup_info") 388 if err != nil { 389 return "", errors.Wrap(err, "get xtrabackup info content") 390 } 391 392 sstGTIDset, err := getGTIDFromSSTInfo(sstContent) 393 if err != nil { 394 return "", err 395 } 396 currGTID := strings.Split(sstGTIDset, ":")[0] 397 398 set, err := getSetFromXtrabackupInfo(currGTID, xtrabackupContent) 399 if err != nil { 400 return "", err 401 } 402 403 return currGTID + ":" + set, nil 404 } 405 406 func getSetFromXtrabackupInfo(gtid string, xtrabackupInfo []byte) (string, error) { 407 gtids, err := getGTIDFromXtrabackup(xtrabackupInfo) 408 if err != nil { 409 return "", errors.Wrap(err, "get gtid from xtrabackup info") 410 } 411 for _, v := range strings.Split(gtids, ",") { 412 valueSplitted := strings.Split(v, ":") 413 if valueSplitted[0] == gtid { 414 return valueSplitted[1], nil 415 } 416 } 417 return "", errors.New("can't find current gtid in xtrabackup file") 418 } 419 420 func getGTIDFromXtrabackup(content []byte) (string, error) { 421 sep := []byte("GTID of the last") 422 startIndex := bytes.Index(content, sep) 423 if startIndex == -1 { 424 return "", errors.New("no gtid data in backup") 425 } 426 newOut := content[startIndex+len(sep):] 427 e := bytes.Index(newOut, []byte("'\n")) 428 if e == -1 { 429 return "", errors.New("can't find gtid data in backup") 430 } 431 432 se := bytes.Index(newOut, []byte("'")) 433 set := newOut[se+1 : e] 434 435 return string(set), nil 436 } 437 438 func getGTIDFromSSTInfo(content []byte) (string, error) { 439 sep := []byte("galera-gtid=") 440 startIndex := bytes.Index(content, sep) 441 if startIndex == -1 { 442 return "", errors.New("no gtid data in backup") 443 } 444 newOut := content[startIndex+len(sep):] 445 e := bytes.Index(newOut, []byte("\n")) 446 if e == -1 { 447 return "", errors.New("can't find gtid data in backup") 448 } 449 return string(newOut[:e]), nil 450 } 451 452 func getDecompressedContent(ctx context.Context, infoObj io.Reader, filename string) ([]byte, error) { 453 tmpDir := os.TempDir() 454 455 cmd := exec.CommandContext(ctx, "xbstream", "-x", "--decompress") 456 cmd.Dir = tmpDir 457 cmd.Stdin = infoObj 458 var outb, errb bytes.Buffer 459 cmd.Stdout = &outb 460 cmd.Stderr = &errb 461 err := cmd.Run() 462 if err != nil { 463 return nil, errors.Wrapf(err, "xbstream cmd run. stderr: %s, stdout: %s", &errb, &outb) 464 } 465 if errb.Len() > 0 { 466 return nil, errors.Errorf("run xbstream error: %s", &errb) 467 } 468 469 decContent, err := os.ReadFile(tmpDir + "/" + filename) 470 if err != nil { 471 return nil, errors.Wrap(err, "read xtrabackup_info file") 472 } 473 474 return decContent, nil 475 } 476 477 func (r *Recoverer) setBinlogs(ctx context.Context) error { 478 list, err := r.storage.ListObjects(ctx, "binlog_") 479 if err != nil { 480 return errors.Wrap(err, "list objects with prefix 'binlog_'") 481 } 482 reverse(list) 483 binlogs := []string{} 484 sourceID := strings.Split(r.startGTID, ":")[0] 485 log.Println("current gtid set is", r.startGTID) 486 for _, binlog := range list { 487 if strings.Contains(binlog, "-gtid-set") { 488 continue 489 } 490 infoObj, err := r.storage.GetObject(ctx, binlog+"-gtid-set") 491 if err != nil { 492 log.Println("Can't get binlog object with gtid set. Name:", binlog, "error", err) 493 continue 494 } 495 content, err := io.ReadAll(infoObj) 496 if err != nil { 497 return errors.Wrapf(err, "read %s gtid-set object", binlog) 498 } 499 binlogGTIDSet := string(content) 500 log.Println("checking current file", " name ", binlog, " gtid ", binlogGTIDSet) 501 502 if len(r.gtid) > 0 && r.recoverType == Transaction { 503 subResult, err := r.db.SubtractGTIDSet(ctx, binlogGTIDSet, r.gtid) 504 if err != nil { 505 return errors.Wrapf(err, "check if '%s' is a subset of '%s", binlogGTIDSet, r.gtid) 506 } 507 if subResult != binlogGTIDSet { 508 set, err := getExtendGTIDSet(binlogGTIDSet, r.gtid) 509 if err != nil { 510 return errors.Wrap(err, "get gtid set for extend") 511 } 512 r.gtidSet = set 513 } 514 if len(r.gtidSet) == 0 { 515 continue 516 } 517 } 518 519 binlogs = append(binlogs, binlog) 520 subResult, err := r.db.SubtractGTIDSet(ctx, r.startGTID, binlogGTIDSet) 521 log.Println("Checking sub result", " binlog gtid ", binlogGTIDSet, " sub result ", subResult) 522 if err != nil { 523 return errors.Wrapf(err, "check if '%s' is a subset of '%s", r.startGTID, binlogGTIDSet) 524 } 525 if subResult != r.startGTID { 526 break 527 } 528 } 529 if len(binlogs) == 0 { 530 return errors.Errorf("no objects for prefix binlog_ or with source_id=%s", sourceID) 531 } 532 reverse(binlogs) 533 r.binlogs = binlogs 534 535 return nil 536 } 537 538 func getExtendGTIDSet(gtidSet, gtid string) (string, error) { 539 if gtidSet == gtid { 540 return gtid, nil 541 } 542 543 s := strings.Split(gtidSet, ":") 544 if len(s) < 2 { 545 return "", errors.Errorf("incorrect source in gtid set %s", gtidSet) 546 } 547 548 eidx := 1 549 e := strings.Split(s[1], "-") 550 if len(e) == 1 { 551 eidx = 0 552 } 553 554 gs := strings.Split(gtid, ":") 555 if len(gs) < 2 { 556 return "", errors.Errorf("incorrect source in gtid set %s", gtid) 557 } 558 559 es := strings.Split(gs[1], "-") 560 561 return gs[0] + ":" + es[0] + "-" + e[eidx], nil 562 } 563 564 func reverse(list []string) { 565 for i := len(list)/2 - 1; i >= 0; i-- { 566 opp := len(list) - 1 - i 567 list[i], list[opp] = list[opp], list[i] 568 } 569 }