github.com/percona/percona-xtradb-cluster-operator@v1.14.0/cmd/pitr/pxc/pxc.go (about)

     1  package pxc
     2  
     3  import (
     4  	"context"
     5  	"database/sql"
     6  	"log"
     7  	"os/exec"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/go-sql-driver/mysql"
    13  	"github.com/pkg/errors"
    14  )
    15  
    16  const UsingPassErrorMessage = `mysqlbinlog: [Warning] Using a password on the command line interface can be insecure.`
    17  
    18  // PXC is a type for working with pxc
    19  type PXC struct {
    20  	db   *sql.DB // handle for work with database
    21  	host string  // host for connection
    22  }
    23  
    24  // NewManager return new manager for work with pxc
    25  func NewPXC(addr string, user, pass string) (*PXC, error) {
    26  	var pxc PXC
    27  
    28  	config := mysql.NewConfig()
    29  	config.User = user
    30  	config.Passwd = pass
    31  	config.Net = "tcp"
    32  	config.Addr = addr + ":33062"
    33  	config.Params = map[string]string{"interpolateParams": "true"}
    34  
    35  	mysqlDB, err := sql.Open("mysql", config.FormatDSN())
    36  	if err != nil {
    37  		return nil, errors.Wrap(err, "cannot connect to host")
    38  	}
    39  
    40  	pxc.db = mysqlDB
    41  	pxc.host = addr
    42  
    43  	return &pxc, nil
    44  }
    45  
    46  // Close is for closing db connection
    47  func (p *PXC) Close() error {
    48  	return p.db.Close()
    49  }
    50  
    51  // GetHost returns pxc host
    52  func (p *PXC) GetHost() string {
    53  	return p.host
    54  }
    55  
    56  // GetGTIDSet return GTID set by binary log file name
    57  func (p *PXC) GetGTIDSet(ctx context.Context, binlogName string) (string, error) {
    58  	//select name from mysql.func where name='get_gtid_set_by_binlog'
    59  	var existFunc string
    60  	nameRow := p.db.QueryRowContext(ctx, "select name from mysql.func where name='get_gtid_set_by_binlog'")
    61  	err := nameRow.Scan(&existFunc)
    62  	if err != nil && err != sql.ErrNoRows {
    63  		return "", errors.Wrap(err, "get udf name")
    64  	}
    65  	if len(existFunc) == 0 {
    66  		_, err = p.db.ExecContext(ctx, "CREATE FUNCTION get_gtid_set_by_binlog RETURNS STRING SONAME 'binlog_utils_udf.so'")
    67  		if err != nil {
    68  			return "", errors.Wrap(err, "create function")
    69  		}
    70  	}
    71  	var binlogSet string
    72  	row := p.db.QueryRowContext(ctx, "SELECT get_gtid_set_by_binlog(?)", binlogName)
    73  	err = row.Scan(&binlogSet)
    74  	if err != nil && !strings.Contains(err.Error(), "Binary log does not exist") {
    75  		return "", errors.Wrap(err, "scan set")
    76  	}
    77  
    78  	return binlogSet, nil
    79  }
    80  
    81  type Binlog struct {
    82  	Name      string
    83  	Size      int64
    84  	Encrypted string
    85  	GTIDSet   GTIDSet
    86  }
    87  
    88  type GTIDSet struct {
    89  	gtidSet string
    90  }
    91  
    92  func NewGTIDSet(gtidSet string) GTIDSet {
    93  	return GTIDSet{gtidSet: gtidSet}
    94  }
    95  
    96  func (s *GTIDSet) IsEmpty() bool {
    97  	return len(s.gtidSet) == 0
    98  }
    99  
   100  func (s *GTIDSet) Raw() string {
   101  	return s.gtidSet
   102  }
   103  
   104  func (s *GTIDSet) List() []string {
   105  	if len(s.gtidSet) == 0 {
   106  		return nil
   107  	}
   108  	list := strings.Split(s.gtidSet, ",")
   109  	sort.Strings(list)
   110  	return list
   111  }
   112  
   113  // GetBinLogList return binary log files list
   114  func (p *PXC) GetBinLogList(ctx context.Context) ([]Binlog, error) {
   115  	rows, err := p.db.QueryContext(ctx, "SHOW BINARY LOGS")
   116  	if err != nil {
   117  		return nil, errors.Wrap(err, "show binary logs")
   118  	}
   119  
   120  	var binlogs []Binlog
   121  	for rows.Next() {
   122  		var b Binlog
   123  		if err := rows.Scan(&b.Name, &b.Size, &b.Encrypted); err != nil {
   124  			return nil, errors.Wrap(err, "scan binlogs")
   125  		}
   126  		binlogs = append(binlogs, b)
   127  	}
   128  
   129  	_, err = p.db.ExecContext(ctx, "FLUSH BINARY LOGS")
   130  	if err != nil {
   131  		return nil, errors.Wrap(err, "flush binary logs")
   132  	}
   133  
   134  	return binlogs, nil
   135  }
   136  
   137  // GetBinLogList return binary log files list
   138  func (p *PXC) GetBinLogNamesList(ctx context.Context) ([]string, error) {
   139  	rows, err := p.db.QueryContext(ctx, "SHOW BINARY LOGS")
   140  	if err != nil {
   141  		return nil, errors.Wrap(err, "show binary logs")
   142  	}
   143  	defer rows.Close()
   144  
   145  	var binlogs []string
   146  	for rows.Next() {
   147  		var b Binlog
   148  		if err := rows.Scan(&b.Name, &b.Size, &b.Encrypted); err != nil {
   149  			return nil, errors.Wrap(err, "scan binlogs")
   150  		}
   151  		binlogs = append(binlogs, b.Name)
   152  	}
   153  
   154  	return binlogs, nil
   155  }
   156  
   157  func (p *PXC) GTIDSubset(ctx context.Context, set1, set2 string) (bool, error) {
   158  	row := p.db.QueryRowContext(ctx, "SELECT GTID_SUBSET(?,?)", set1, set2)
   159  	var result int
   160  	if err := row.Scan(&result); err != nil {
   161  		return false, errors.Wrap(err, "scan result")
   162  	}
   163  
   164  	return result == 1, nil
   165  }
   166  
   167  // GetBinLogFirstTimestamp return binary log file first timestamp
   168  func (p *PXC) GetBinLogFirstTimestamp(ctx context.Context, binlog string) (string, error) {
   169  	var existFunc string
   170  	nameRow := p.db.QueryRowContext(ctx, "select name from mysql.func where name='get_first_record_timestamp_by_binlog'")
   171  	err := nameRow.Scan(&existFunc)
   172  	if err != nil && err != sql.ErrNoRows {
   173  		return "", errors.Wrap(err, "get udf name")
   174  	}
   175  	if len(existFunc) == 0 {
   176  		_, err = p.db.ExecContext(ctx, "CREATE FUNCTION get_first_record_timestamp_by_binlog RETURNS INTEGER SONAME 'binlog_utils_udf.so'")
   177  		if err != nil {
   178  			return "", errors.Wrap(err, "create function")
   179  		}
   180  	}
   181  	var timestamp string
   182  	row := p.db.QueryRowContext(ctx, "SELECT get_first_record_timestamp_by_binlog(?) DIV 1000000", binlog)
   183  
   184  	err = row.Scan(&timestamp)
   185  	if err != nil {
   186  		return "", errors.Wrap(err, "scan binlog timestamp")
   187  	}
   188  
   189  	return timestamp, nil
   190  }
   191  
   192  // GetBinLogLastTimestamp return binary log file last timestamp
   193  func (p *PXC) GetBinLogLastTimestamp(ctx context.Context, binlog string) (string, error) {
   194  	var existFunc string
   195  	nameRow := p.db.QueryRowContext(ctx, "select name from mysql.func where name='get_last_record_timestamp_by_binlog'")
   196  	err := nameRow.Scan(&existFunc)
   197  	if err != nil && err != sql.ErrNoRows {
   198  		return "", errors.Wrap(err, "get udf name")
   199  	}
   200  	if len(existFunc) == 0 {
   201  		_, err = p.db.ExecContext(ctx, "CREATE FUNCTION get_last_record_timestamp_by_binlog RETURNS INTEGER SONAME 'binlog_utils_udf.so'")
   202  		if err != nil {
   203  			return "", errors.Wrap(err, "create function")
   204  		}
   205  	}
   206  	var timestamp string
   207  	row := p.db.QueryRowContext(ctx, "SELECT get_last_record_timestamp_by_binlog(?) DIV 1000000", binlog)
   208  
   209  	err = row.Scan(&timestamp)
   210  	if err != nil {
   211  		return "", errors.Wrap(err, "scan binlog timestamp")
   212  	}
   213  
   214  	return timestamp, nil
   215  }
   216  
   217  func (p *PXC) SubtractGTIDSet(ctx context.Context, set, subSet string) (string, error) {
   218  	var result string
   219  	row := p.db.QueryRowContext(ctx, "SELECT GTID_SUBTRACT(?,?)", set, subSet)
   220  	err := row.Scan(&result)
   221  	if err != nil {
   222  		return "", errors.Wrap(err, "scan gtid subtract result")
   223  	}
   224  
   225  	return result, nil
   226  }
   227  
   228  func getNodesByServiceName(ctx context.Context, pxcServiceName string) ([]string, error) {
   229  	cmd := exec.CommandContext(ctx, "peer-list", "-on-start=/usr/bin/get-pxc-state", "-service="+pxcServiceName)
   230  	out, err := cmd.CombinedOutput()
   231  	if err != nil {
   232  		return nil, errors.Wrap(err, "get peer-list output")
   233  	}
   234  	return strings.Split(string(out), "node:"), nil
   235  }
   236  
   237  func GetPXCFirstHost(ctx context.Context, pxcServiceName string) (string, error) {
   238  	nodes, err := getNodesByServiceName(ctx, pxcServiceName)
   239  	if err != nil {
   240  		return "", errors.Wrap(err, "get nodes by service name")
   241  	}
   242  	sort.Strings(nodes)
   243  	lastHost := ""
   244  	for _, node := range nodes {
   245  		if strings.Contains(node, "wsrep_ready:ON:wsrep_connected:ON:wsrep_local_state_comment:Synced:wsrep_cluster_status:Primary") {
   246  			nodeArr := strings.Split(node, ":")
   247  			lastHost = nodeArr[0]
   248  			break
   249  		}
   250  	}
   251  	if len(lastHost) == 0 {
   252  		return "", errors.New("can't find host")
   253  	}
   254  
   255  	return lastHost, nil
   256  }
   257  
   258  func GetPXCOldestBinlogHost(ctx context.Context, pxcServiceName, user, pass string) (string, error) {
   259  	nodes, err := getNodesByServiceName(ctx, pxcServiceName)
   260  	if err != nil {
   261  		return "", errors.Wrap(err, "get nodes by service name")
   262  	}
   263  
   264  	var oldestHost string
   265  	var oldestTS int64
   266  	for _, node := range nodes {
   267  		if strings.Contains(node, "wsrep_ready:ON:wsrep_connected:ON:wsrep_local_state_comment:Synced:wsrep_cluster_status:Primary") {
   268  			nodeArr := strings.Split(node, ":")
   269  			binlogTime, err := getBinlogTime(ctx, nodeArr[0], user, pass)
   270  			if err != nil {
   271  				log.Printf("ERROR: get binlog time %v", err)
   272  				continue
   273  			}
   274  			if len(oldestHost) == 0 || oldestTS > 0 && binlogTime < oldestTS {
   275  				oldestHost = nodeArr[0]
   276  				oldestTS = binlogTime
   277  			}
   278  
   279  		}
   280  	}
   281  
   282  	if len(oldestHost) == 0 {
   283  		return "", errors.New("can't find host")
   284  	}
   285  
   286  	return oldestHost, nil
   287  }
   288  
   289  func getBinlogTime(ctx context.Context, host, user, pass string) (int64, error) {
   290  	db, err := NewPXC(host, user, pass)
   291  	if err != nil {
   292  		return 0, errors.Errorf("creating connection for host %s: %v", host, err)
   293  	}
   294  	defer db.Close()
   295  	list, err := db.GetBinLogNamesList(ctx)
   296  	if err != nil {
   297  		return 0, errors.Errorf("get binlog list for host %s: %v", host, err)
   298  	}
   299  	if len(list) == 0 {
   300  		return 0, errors.Errorf("get binlog list for host %s: no binlogs found", host)
   301  	}
   302  	var binlogTime int64
   303  	for _, binlogName := range list {
   304  		binlogTime, err = getBinlogTimeByName(ctx, db, binlogName)
   305  		if err != nil {
   306  			log.Printf("ERROR: get binlog timestamp for binlog %s host %s: %v", binlogName, host, err)
   307  			continue
   308  		}
   309  		if binlogTime > 0 {
   310  			break
   311  		}
   312  	}
   313  	if binlogTime == 0 {
   314  		return 0, errors.Errorf("get binlog oldest timestamp for host %s: no binlogs timestamp found", host)
   315  	}
   316  
   317  	return binlogTime, nil
   318  }
   319  
   320  func getBinlogTimeByName(ctx context.Context, db *PXC, binlogName string) (int64, error) {
   321  	ts, err := db.GetBinLogFirstTimestamp(ctx, binlogName)
   322  	if err != nil {
   323  		return 0, errors.Wrap(err, "get binlog first timestamp")
   324  	}
   325  	binlogTime, err := strconv.ParseInt(ts, 10, 64)
   326  	if err != nil {
   327  		return 0, errors.Wrap(err, "parse timestamp")
   328  	}
   329  
   330  	return binlogTime, nil
   331  }
   332  
   333  func (p *PXC) DropCollectorFunctions(ctx context.Context) error {
   334  	_, err := p.db.ExecContext(ctx, "DROP FUNCTION IF EXISTS get_first_record_timestamp_by_binlog")
   335  	if err != nil {
   336  		return errors.Wrap(err, "drop get_first_record_timestamp_by_binlog function")
   337  	}
   338  	_, err = p.db.ExecContext(ctx, "DROP FUNCTION IF EXISTS get_binlog_by_gtid_set")
   339  	if err != nil {
   340  		return errors.Wrap(err, "drop get_binlog_by_gtid_set function")
   341  	}
   342  
   343  	_, err = p.db.ExecContext(ctx, "DROP FUNCTION IF EXISTS get_gtid_set_by_binlog")
   344  	if err != nil {
   345  		return errors.Wrap(err, "drop get_gtid_set_by_binlog function")
   346  	}
   347  
   348  	return nil
   349  }