Merge branch '3.0' into PT-141

This commit is contained in:
Carlos Salguero
2017-05-23 14:21:03 -03:00
23 changed files with 138 additions and 69 deletions

View File

@@ -1,5 +1,9 @@
Changelog for Percona Toolkit Changelog for Percona Toolkit
v3.0.4
* Fixed bug PT-138 : Added --output-format option to pt-mongodb-summary
v3.0.3 v3.0.3
* Fixed bug PT-133 : Sandbox won't start correctly if autocommit=0 in my.cnf * Fixed bug PT-133 : Sandbox won't start correctly if autocommit=0 in my.cnf

View File

@@ -45,7 +45,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -43,7 +43,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -42,7 +42,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -38,7 +38,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -39,7 +39,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -35,7 +35,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -37,7 +37,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -44,7 +44,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -45,7 +45,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -47,7 +47,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -55,7 +55,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';
@@ -10053,8 +10053,13 @@ sub find_renamed_cols {
/x; /x;
my $table_ident = qr/$unquoted_ident|`$quoted_ident`|"$ansi_quotes_ident"/; my $table_ident = qr/$unquoted_ident|`$quoted_ident`|"$ansi_quotes_ident"/;
my $alter_change_col_re = qr/\bCHANGE \s+ (?:COLUMN \s+)? (?:COMMENT\s+[^\]['"].*?[^\]['"])?
($table_ident) \s+ ($table_ident)/ix; # remove comments
$alter =~ s/^(.*?)\s+COMMENT\s+'(.*?[^\\]')+(.*)/$1$3/;
$alter =~ s/^(.*?)\s+COMMENT\s+"(.*?[^\\]")+(.*)/$1$3/;
my $alter_change_col_re = qr/\bCHANGE \s+ (?:COLUMN \s+)?
($table_ident) \s+ ($table_ident)/ix;
my %renames; my %renames;
while ( $alter =~ /$alter_change_col_re/g ) { while ( $alter =~ /$alter_change_col_re/g ) {

View File

@@ -64,7 +64,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -40,7 +40,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -41,7 +41,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -57,7 +57,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -55,7 +55,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -61,7 +61,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -44,7 +44,7 @@ BEGIN {
{ {
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -18,7 +18,7 @@
# ########################################################################### # ###########################################################################
package Percona::Toolkit; package Percona::Toolkit;
our $VERSION = '3.0.2'; our $VERSION = '3.0.3';
use strict; use strict;
use warnings FATAL => 'all'; use warnings FATAL => 'all';

View File

@@ -1,11 +1,13 @@
package main package main
import ( import (
"bytes"
"encoding/json"
"fmt" "fmt"
"html/template"
"net" "net"
"os" "os"
"strings" "strings"
"text/template"
"time" "time"
version "github.com/hashicorp/go-version" version "github.com/hashicorp/go-version"
@@ -33,6 +35,7 @@ const (
DEFAULT_LOGLEVEL = "warn" DEFAULT_LOGLEVEL = "warn"
DEFAULT_RUNNINGOPSINTERVAL = 1000 // milliseconds DEFAULT_RUNNINGOPSINTERVAL = 1000 // milliseconds
DEFAULT_RUNNINGOPSSAMPLES = 5 DEFAULT_RUNNINGOPSSAMPLES = 5
DEFAULT_OUTPUT_FORMAT = "text"
) )
var ( var (
@@ -130,12 +133,24 @@ type options struct {
Version bool Version bool
NoVersionCheck bool NoVersionCheck bool
NoRunningOps bool NoRunningOps bool
OutputFormat string
RunningOpsSamples int RunningOpsSamples int
RunningOpsInterval int RunningOpsInterval int
SSLCAFile string SSLCAFile string
SSLPEMKeyFile string SSLPEMKeyFile string
} }
type collectedInfo struct {
BalancerStats *proto.BalancerStats
ClusterWideInfo *clusterwideInfo
OplogInfo []proto.OplogInfo
ReplicaMembers []proto.Members
RunningOps *opCounters
SecuritySettings *security
HostInfo *hostInfo
Errors []string
}
func main() { func main() {
opts, err := parseFlags() opts, err := parseFlags()
@@ -210,77 +225,105 @@ func main() {
defer session.Close() defer session.Close()
session.SetMode(mgo.Monotonic, true) session.SetMode(mgo.Monotonic, true)
hostInfo, err := GetHostinfo(session) ci := &collectedInfo{}
ci.HostInfo, err = GetHostinfo(session)
if err != nil { if err != nil {
message := fmt.Sprintf("Cannot get host info for %q: %s", di.Addrs[0], err.Error()) message := fmt.Sprintf("Cannot get host info for %q: %s", di.Addrs[0], err.Error())
log.Errorf(message) log.Errorf(message)
os.Exit(2) os.Exit(2)
} }
if replicaMembers, err := util.GetReplicasetMembers(dialer, di); err != nil { if ci.ReplicaMembers, err = util.GetReplicasetMembers(dialer, di); err != nil {
log.Warnf("[Error] cannot get replicaset members: %v\n", err) log.Warnf("[Error] cannot get replicaset members: %v\n", err)
os.Exit(2) os.Exit(2)
} else {
log.Debugf("replicaMembers:\n%+v\n", replicaMembers)
t := template.Must(template.New("replicas").Parse(templates.Replicas))
t.Execute(os.Stdout, replicaMembers)
} }
log.Debugf("replicaMembers:\n%+v\n", ci.ReplicaMembers)
// Host Info
t := template.Must(template.New("hosttemplateData").Parse(templates.HostInfo))
t.Execute(os.Stdout, hostInfo)
if opts.RunningOpsSamples > 0 && opts.RunningOpsInterval > 0 { if opts.RunningOpsSamples > 0 && opts.RunningOpsInterval > 0 {
if rops, err := GetOpCountersStats(session, opts.RunningOpsSamples, time.Duration(opts.RunningOpsInterval)*time.Millisecond); err != nil { if ci.RunningOps, err = GetOpCountersStats(session, opts.RunningOpsSamples, time.Duration(opts.RunningOpsInterval)*time.Millisecond); err != nil {
log.Printf("[Error] cannot get Opcounters stats: %v\n", err) log.Printf("[Error] cannot get Opcounters stats: %v\n", err)
} else {
t := template.Must(template.New("runningOps").Parse(templates.RunningOps))
t.Execute(os.Stdout, rops)
} }
} }
if hostInfo != nil { if ci.HostInfo != nil {
if security, err := GetSecuritySettings(session, hostInfo.Version); err != nil { if ci.SecuritySettings, err = GetSecuritySettings(session, ci.HostInfo.Version); err != nil {
log.Errorf("[Error] cannot get security settings: %v\n", err) log.Errorf("[Error] cannot get security settings: %v\n", err)
} else {
t := template.Must(template.New("ssl").Parse(templates.Security))
t.Execute(os.Stdout, security)
} }
} else { } else {
log.Warn("Cannot check security settings since host info is not available (permissions?)") log.Warn("Cannot check security settings since host info is not available (permissions?)")
} }
if oplogInfo, err := oplog.GetOplogInfo(hostnames, di); err != nil { if ci.OplogInfo, err = oplog.GetOplogInfo(hostnames, di); err != nil {
log.Info("Cannot get Oplog info: %v\n", err) log.Info("Cannot get Oplog info: %v\n", err)
} else { } else {
if len(oplogInfo) > 0 { if len(ci.OplogInfo) == 0 {
t := template.Must(template.New("oplogInfo").Parse(templates.Oplog))
t.Execute(os.Stdout, oplogInfo[0])
} else {
log.Info("oplog info is empty. Skipping") log.Info("oplog info is empty. Skipping")
} else {
ci.OplogInfo = ci.OplogInfo[:1]
} }
} }
// individual servers won't know about this info // individual servers won't know about this info
if hostInfo.NodeType == "mongos" { if ci.HostInfo.NodeType == "mongos" {
if cwi, err := GetClusterwideInfo(session); err != nil { if ci.ClusterWideInfo, err = GetClusterwideInfo(session); err != nil {
log.Printf("[Error] cannot get cluster wide info: %v\n", err) log.Printf("[Error] cannot get cluster wide info: %v\n", err)
} else {
t := template.Must(template.New("clusterwide").Parse(templates.Clusterwide))
t.Execute(os.Stdout, cwi)
} }
} }
if hostInfo.NodeType == "mongos" { if ci.HostInfo.NodeType == "mongos" {
if bs, err := GetBalancerStats(session); err != nil { if ci.BalancerStats, err = GetBalancerStats(session); err != nil {
log.Printf("[Error] cannot get balancer stats: %v\n", err) log.Printf("[Error] cannot get balancer stats: %v\n", err)
} else {
t := template.Must(template.New("balancer").Parse(templates.BalancerStats))
t.Execute(os.Stdout, bs)
} }
} }
out, err := formatResults(ci, opts.OutputFormat)
if err != nil {
log.Errorf("Cannot format the results: %s", err.Error())
os.Exit(1)
}
fmt.Println(string(out))
}
func formatResults(ci *collectedInfo, format string) ([]byte, error) {
var buf *bytes.Buffer
switch format {
case "json":
b, err := json.MarshalIndent(ci, "", " ")
if err != nil {
return nil, fmt.Errorf("[Error] Cannot convert results to json: %s", err.Error())
}
buf = bytes.NewBuffer(b)
default:
buf = new(bytes.Buffer)
t := template.Must(template.New("replicas").Parse(templates.Replicas))
t.Execute(buf, ci.ReplicaMembers)
t = template.Must(template.New("hosttemplateData").Parse(templates.HostInfo))
t.Execute(buf, ci.HostInfo)
t = template.Must(template.New("runningOps").Parse(templates.RunningOps))
t.Execute(buf, ci.RunningOps)
t = template.Must(template.New("ssl").Parse(templates.Security))
t.Execute(buf, ci.SecuritySettings)
if ci.OplogInfo != nil && len(ci.OplogInfo) > 0 {
t = template.Must(template.New("oplogInfo").Parse(templates.Oplog))
t.Execute(buf, ci.OplogInfo[0])
}
t = template.Must(template.New("clusterwide").Parse(templates.Clusterwide))
t.Execute(buf, ci.ClusterWideInfo)
t = template.Must(template.New("balancer").Parse(templates.BalancerStats))
t.Execute(buf, ci.BalancerStats)
}
return buf.Bytes(), nil
} }
func GetHostinfo(session pmgo.SessionManager) (*hostInfo, error) { func GetHostinfo(session pmgo.SessionManager) (*hostInfo, error) {
@@ -472,6 +515,7 @@ func GetSecuritySettings(session pmgo.SessionManager, ver string) (*security, er
// Lets try both // Lets try both
newSession := session.Clone() newSession := session.Clone()
defer newSession.Close() defer newSession.Close()
newSession.SetMode(mgo.Strong, true) newSession.SetMode(mgo.Strong, true)
if s.Users, s.Roles, err = getUserRolesCount(newSession); err != nil { if s.Users, s.Roles, err = getUserRolesCount(newSession); err != nil {
@@ -811,6 +855,7 @@ func parseFlags() (*options, error) {
RunningOpsSamples: DEFAULT_RUNNINGOPSSAMPLES, RunningOpsSamples: DEFAULT_RUNNINGOPSSAMPLES,
RunningOpsInterval: DEFAULT_RUNNINGOPSINTERVAL, // milliseconds RunningOpsInterval: DEFAULT_RUNNINGOPSINTERVAL, // milliseconds
AuthDB: DEFAULT_AUTHDB, AuthDB: DEFAULT_AUTHDB,
OutputFormat: DEFAULT_OUTPUT_FORMAT,
} }
gop := getopt.New() gop := getopt.New()
@@ -821,8 +866,9 @@ func parseFlags() (*options, error) {
gop.StringVarLong(&opts.User, "username", 'u', "", "Username to use for optional MongoDB authentication") gop.StringVarLong(&opts.User, "username", 'u', "", "Username to use for optional MongoDB authentication")
gop.StringVarLong(&opts.Password, "password", 'p', "", "Password to use for optional MongoDB authentication").SetOptional() gop.StringVarLong(&opts.Password, "password", 'p', "", "Password to use for optional MongoDB authentication").SetOptional()
gop.StringVarLong(&opts.AuthDB, "authenticationDatabase", 'a', "admin", gop.StringVarLong(&opts.AuthDB, "authenticationDatabase", 'a', "admin",
"Databaae to use for optional MongoDB authentication. Default: admin") "Database to use for optional MongoDB authentication. Default: admin")
gop.StringVarLong(&opts.LogLevel, "log-level", 'l', "error", "Log level: panic, fatal, error, warn, info, debug. Default: error") gop.StringVarLong(&opts.LogLevel, "log-level", 'l', "error", "Log level: panic, fatal, error, warn, info, debug. Default: error")
gop.StringVarLong(&opts.OutputFormat, "output-format", 'f', "text", "Output format: text, json. Default: text")
gop.IntVarLong(&opts.RunningOpsSamples, "running-ops-samples", 's', gop.IntVarLong(&opts.RunningOpsSamples, "running-ops-samples", 's',
fmt.Sprintf("Number of samples to collect for running ops. Default: %d", opts.RunningOpsSamples)) fmt.Sprintf("Number of samples to collect for running ops. Default: %d", opts.RunningOpsSamples))
@@ -852,6 +898,9 @@ func parseFlags() (*options, error) {
gop.PrintUsage(os.Stdout) gop.PrintUsage(os.Stdout)
return nil, nil return nil, nil
} }
if opts.OutputFormat != "json" && opts.OutputFormat != "text" {
log.Infof("Invalid output format '%s'. Using text format", opts.OutputFormat)
}
return opts, nil return opts, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
mgo "gopkg.in/mgo.v2"
"gopkg.in/mgo.v2/bson" "gopkg.in/mgo.v2/bson"
"gopkg.in/mgo.v2/dbtest" "gopkg.in/mgo.v2/dbtest"
@@ -205,6 +206,9 @@ func TestSecurityOpts(t *testing.T) {
session.EXPECT().DB("admin").Return(database) session.EXPECT().DB("admin").Return(database)
database.EXPECT().Run(bson.D{{"getCmdLineOpts", 1}, {"recordStats", 1}}, gomock.Any()).SetArg(1, cmd) database.EXPECT().Run(bson.D{{"getCmdLineOpts", 1}, {"recordStats", 1}}, gomock.Any()).SetArg(1, cmd)
session.EXPECT().Clone().Return(session)
session.EXPECT().SetMode(mgo.Strong, true)
session.EXPECT().DB("admin").Return(database) session.EXPECT().DB("admin").Return(database)
database.EXPECT().C("system.users").Return(usersCol) database.EXPECT().C("system.users").Return(usersCol)
usersCol.EXPECT().Count().Return(1, nil) usersCol.EXPECT().Count().Return(1, nil)
@@ -212,6 +216,7 @@ func TestSecurityOpts(t *testing.T) {
session.EXPECT().DB("admin").Return(database) session.EXPECT().DB("admin").Return(database)
database.EXPECT().C("system.roles").Return(rolesCol) database.EXPECT().C("system.roles").Return(rolesCol)
rolesCol.EXPECT().Count().Return(2, nil) rolesCol.EXPECT().Count().Return(2, nil)
session.EXPECT().Close().Return()
got, err := GetSecuritySettings(session, "3.2") got, err := GetSecuritySettings(session, "3.2")
@@ -392,21 +397,25 @@ func TestParseArgs(t *testing.T) {
{ {
args: []string{TOOLNAME}, // arg[0] is the command itself args: []string{TOOLNAME}, // arg[0] is the command itself
want: &options{ want: &options{
Host: DEFAULT_HOST, Host: DEFAULT_HOST,
LogLevel: DEFAULT_LOGLEVEL, LogLevel: DEFAULT_LOGLEVEL,
AuthDB: DEFAULT_AUTHDB, AuthDB: DEFAULT_AUTHDB,
RunningOpsSamples: DEFAULT_RUNNINGOPSSAMPLES,
RunningOpsInterval: DEFAULT_RUNNINGOPSINTERVAL,
OutputFormat: "text",
}, },
}, },
{ {
args: []string{TOOLNAME, "zapp.brannigan.net:27018/samples", "--help"}, args: []string{TOOLNAME, "zapp.brannigan.net:27018/samples", "--help"},
want: &options{ want: nil,
Host: "zapp.brannigan.net:27018/samples",
LogLevel: DEFAULT_LOGLEVEL,
AuthDB: DEFAULT_AUTHDB,
Help: true,
},
}, },
} }
// Capture stdout to not to show help
old := os.Stdout // keep backup of the real stdout
_, w, _ := os.Pipe()
os.Stdout = w
for i, test := range tests { for i, test := range tests {
getopt.Reset() getopt.Reset()
os.Args = test.args os.Args = test.args
@@ -419,4 +428,6 @@ func TestParseArgs(t *testing.T) {
} }
} }
os.Stdout = old
} }

View File

@@ -610,7 +610,7 @@ $sb->load_file('master', "$sample/bug-1613915.sql");
$output = output( $output = output(
sub { pt_online_schema_change::main(@args, "$master_dsn,D=test,t=o1", sub { pt_online_schema_change::main(@args, "$master_dsn,D=test,t=o1",
'--execute', '--execute',
'--alter', "ADD COLUMN c INT COMMENT 'change plus more than one word'", '--alter', "ADD COLUMN c INT COMMENT 'change \"plus\" more than one word'",
'--chunk-size', '10', '--no-check-alter', '--chunk-size', '10', '--no-check-alter',
), ),
}, },
@@ -633,7 +633,7 @@ is(
$rows = $master_dbh->selectrow_arrayref("SHOW CREATE TABLE test.o1"); $rows = $master_dbh->selectrow_arrayref("SHOW CREATE TABLE test.o1");
like( like(
$rows->[1], $rows->[1],
qr/COMMENT 'change plus more than one word'/, qr/COMMENT 'change "plus" more than one word'/,
"recognize comments", "recognize comments",
); );