diff options
Diffstat (limited to 'src/mongo/gotools/common/options')
-rw-r--r-- | src/mongo/gotools/common/options/options.go | 346 | ||||
-rw-r--r-- | src/mongo/gotools/common/options/options_gssapi.go | 12 | ||||
-rw-r--r-- | src/mongo/gotools/common/options/options_ssl.go | 18 | ||||
-rw-r--r-- | src/mongo/gotools/common/options/options_test.go | 75 |
4 files changed, 451 insertions, 0 deletions
diff --git a/src/mongo/gotools/common/options/options.go b/src/mongo/gotools/common/options/options.go new file mode 100644 index 00000000000..8962be7f8d7 --- /dev/null +++ b/src/mongo/gotools/common/options/options.go @@ -0,0 +1,346 @@ +// Package options implements command-line options that are used by all of +// the mongo tools. +package options + +import ( + "github.com/jessevdk/go-flags" + "github.com/mongodb/mongo-tools/common/log" + + "fmt" + "os" + "regexp" + "runtime" + "strconv" + "strings" +) + +// Gitspec that the tool was built with. Needs to be set using -ldflags +var ( + VersionStr = "built-without-version-string" + Gitspec = "built-without-git-spec" +) + +// Struct encompassing all of the options that are reused across tools: "help", +// "version", verbosity settings, ssl settings, etc. +type ToolOptions struct { + + // The name of the tool + AppName string + + // The version of the tool + VersionStr string + + // Sub-option types + *General + *Verbosity + *Connection + *SSL + *Auth + *Kerberos + *Namespace + + // Force direct connection to the server and disable the + // drivers automatic repl set discovery logic. + Direct bool + + // ReplicaSetName, if specified, will prevent the obtained session from + // communicating with any server which is not part of a replica set + // with the given name. The default is to communicate with any server + // specified or discovered via the servers contacted. + ReplicaSetName string + + // for caching the parser + parser *flags.Parser +} + +type Namespace struct { + // Specified database and collection + DB string `short:"d" long:"db" value-name:"<database-name>" description:"database to use"` + Collection string `short:"c" long:"collection" value-name:"<collection-name>" description:"collection to use"` +} + +// Struct holding generic options +type General struct { + Help bool `long:"help" description:"print usage"` + Version bool `long:"version" description:"print the tool version and exit"` + + MaxProcs int `long:"numThreads" default:"0" hidden:"true"` +} + +// Struct holding verbosity-related options +type Verbosity struct { + SetVerbosity func(string) `short:"v" long:"verbose" value-name:"<level>" description:"more detailed log output (include multiple times for more verbosity, e.g. -vvvvv, or specify a numeric value, e.g. --verbose=N)" optional:"true" optional-value:""` + Quiet bool `long:"quiet" description:"hide all log output"` + VLevel int `no-flag:"true"` +} + +func (v Verbosity) Level() int { + return v.VLevel +} + +func (v Verbosity) IsQuiet() bool { + return v.Quiet +} + +// Struct holding connection-related options +type Connection struct { + Host string `short:"h" long:"host" value-name:"<hostname>" description:"mongodb host to connect to (setname/host1,host2 for replica sets)"` + Port string `long:"port" value-name:"<port>" description:"server port (can also use --host hostname:port)"` + + Timeout int `long:"dialTimeout" default:"3" hidden:"true" description:"dial timeout in seconds"` +} + +// Struct holding ssl-related options +type SSL struct { + UseSSL bool `long:"ssl" description:"connect to a mongod or mongos that has ssl enabled"` + SSLCAFile string `long:"sslCAFile" value-name:"<filename>" description:"the .pem file containing the root certificate chain from the certificate authority"` + SSLPEMKeyFile string `long:"sslPEMKeyFile" value-name:"<filename>" description:"the .pem file containing the certificate and key"` + SSLPEMKeyPassword string `long:"sslPEMKeyPassword" value-name:"<password>" description:"the password to decrypt the sslPEMKeyFile, if necessary"` + SSLCRLFile string `long:"sslCRLFile" value-name:"<filename>" description:"the .pem file containing the certificate revocation list"` + SSLAllowInvalidCert bool `long:"sslAllowInvalidCertificates" description:"bypass the validation for server certificates"` + SSLAllowInvalidHost bool `long:"sslAllowInvalidHostnames" description:"bypass the validation for server name"` + SSLFipsMode bool `long:"sslFIPSMode" description:"use FIPS mode of the installed openssl library"` +} + +// Struct holding auth-related options +type Auth struct { + Username string `short:"u" value-name:"<username>" long:"username" description:"username for authentication"` + Password string `short:"p" value-name:"<password>" long:"password" description:"password for authentication"` + Source string `long:"authenticationDatabase" value-name:"<database-name>" description:"database that holds the user's credentials"` + Mechanism string `long:"authenticationMechanism" value-name:"<mechanism>" description:"authentication mechanism to use"` +} + +// Struct for Kerberos/GSSAPI-specific options +type Kerberos struct { + Service string `long:"gssapiServiceName" value-name:"<service-name>" description:"service name to use when authenticating using GSSAPI/Kerberos ('mongodb' by default)"` + ServiceHost string `long:"gssapiHostName" value-name:"<host-name>" description:"hostname to use when authenticating using GSSAPI/Kerberos (remote server's address by default)"` +} + +type OptionRegistrationFunction func(o *ToolOptions) error + +var ConnectionOptFunctions []OptionRegistrationFunction + +type EnabledOptions struct { + Auth bool + Connection bool + Namespace bool +} + +func parseVal(val string) int { + idx := strings.Index(val, "=") + ret, err := strconv.Atoi(val[idx+1:]) + if err != nil { + panic(fmt.Errorf("value was not a valid integer: %v", err)) + } + return ret +} + +// Ask for a new instance of tool options +func New(appName, usageStr string, enabled EnabledOptions) *ToolOptions { + opts := &ToolOptions{ + AppName: appName, + VersionStr: VersionStr, + + General: &General{}, + Verbosity: &Verbosity{}, + Connection: &Connection{}, + SSL: &SSL{}, + Auth: &Auth{}, + Namespace: &Namespace{}, + Kerberos: &Kerberos{}, + parser: flags.NewNamedParser( + fmt.Sprintf("%v %v", appName, usageStr), flags.None), + } + + // Called when -v or --verbose is parsed + opts.SetVerbosity = func(val string) { + if i, err := strconv.Atoi(val); err == nil { + opts.VLevel = opts.VLevel + i // -v=N or --verbose=N + } else if matched, _ := regexp.MatchString(`^v+$`, val); matched { + opts.VLevel = opts.VLevel + len(val) + 1 // Handles the -vvv cases + } else if matched, _ := regexp.MatchString(`^v+=[0-9]$`, val); matched { + opts.VLevel = parseVal(val) // I.e. -vv=3 + } else if val == "" { + opts.VLevel = opts.VLevel + 1 // Increment for every occurrence of flag + } else { + log.Logvf(log.Always, "Invalid verbosity value given") + os.Exit(-1) + } + } + + opts.parser.UnknownOptionHandler = opts.handleUnknownOption + + if _, err := opts.parser.AddGroup("general options", "", opts.General); err != nil { + panic(fmt.Errorf("couldn't register general options: %v", err)) + } + if _, err := opts.parser.AddGroup("verbosity options", "", opts.Verbosity); err != nil { + panic(fmt.Errorf("couldn't register verbosity options: %v", err)) + } + + if enabled.Connection { + if _, err := opts.parser.AddGroup("connection options", "", opts.Connection); err != nil { + panic(fmt.Errorf("couldn't register connection options: %v", err)) + } + + // Register options that were enabled at compile time with build tags (ssl, sasl) + for _, optionRegistrationFunction := range ConnectionOptFunctions { + if err := optionRegistrationFunction(opts); err != nil { + panic(fmt.Errorf("couldn't register command-line options: %v", err)) + } + } + } + + if enabled.Auth { + if _, err := opts.parser.AddGroup("authentication options", "", opts.Auth); err != nil { + panic(fmt.Errorf("couldn't register auth options")) + } + } + if enabled.Namespace { + if _, err := opts.parser.AddGroup("namespace options", "", opts.Namespace); err != nil { + panic(fmt.Errorf("couldn't register namespace options")) + } + } + + if opts.MaxProcs <= 0 { + opts.MaxProcs = runtime.NumCPU() + } + log.Logvf(log.Info, "Setting num cpus to %v", opts.MaxProcs) + runtime.GOMAXPROCS(opts.MaxProcs) + return opts +} + +// UseReadOnlyHostDescription changes the help description of the --host arg to +// not mention the shard/host:port format used in the data-mutating tools +func (o *ToolOptions) UseReadOnlyHostDescription() { + hostOpt := o.parser.FindOptionByLongName("host") + hostOpt.Description = "mongodb host(s) to connect to (use commas to delimit hosts)" +} + +// FindOptionByLongName finds an option in any of the added option groups by +// matching its long name; useful for modifying the attributes (e.g. description +// or name) of an option +func (o *ToolOptions) FindOptionByLongName(name string) *flags.Option { + return o.parser.FindOptionByLongName(name) +} + +// Print the usage message for the tool to stdout. Returns whether or not the +// help flag is specified. +func (o *ToolOptions) PrintHelp(force bool) bool { + if o.Help || force { + o.parser.WriteHelp(os.Stdout) + } + return o.Help +} + +type versionInfo struct { + key, value string +} + +var versionInfos []versionInfo + +// Print the tool version to stdout. Returns whether or not the version flag +// is specified. +func (o *ToolOptions) PrintVersion() bool { + if o.Version { + fmt.Printf("%v version: %v\n", o.AppName, o.VersionStr) + fmt.Printf("git version: %v\n", Gitspec) + fmt.Printf("Go version: %v\n", runtime.Version()) + fmt.Printf(" os: %v\n", runtime.GOOS) + fmt.Printf(" arch: %v\n", runtime.GOARCH) + fmt.Printf(" compiler: %v\n", runtime.Compiler) + for _, info := range versionInfos { + fmt.Printf("%s: %s\n", info.key, info.value) + } + } + return o.Version +} + +// Interface for extra options that need to be used by specific tools +type ExtraOptions interface { + // Name specifying what type of options these are + Name() string +} + +func (auth *Auth) RequiresExternalDB() bool { + return auth.Mechanism == "GSSAPI" || auth.Mechanism == "PLAIN" || auth.Mechanism == "MONGODB-X509" +} + +// ShouldAskForPassword returns true if the user specifies a username flag +// but no password, and the authentication mechanism requires a password. +func (auth *Auth) ShouldAskForPassword() bool { + return auth.Username != "" && auth.Password == "" && + !(auth.Mechanism == "MONGODB-X509" || auth.Mechanism == "GSSAPI") +} + +// Get the authentication database to use. Should be the value of +// --authenticationDatabase if it's provided, otherwise, the database that's +// specified in the tool's --db arg. +func (o *ToolOptions) GetAuthenticationDatabase() string { + if o.Auth.Source != "" { + return o.Auth.Source + } else if o.Auth.RequiresExternalDB() { + return "$external" + } else if o.Namespace != nil && o.Namespace.DB != "" { + return o.Namespace.DB + } + return "" +} + +// AddOptions registers an additional options group to this instance +func (o *ToolOptions) AddOptions(opts ExtraOptions) { + _, err := o.parser.AddGroup(opts.Name()+" options", "", opts) + if err != nil { + panic(fmt.Sprintf("error setting command line options for %v: %v", + opts.Name(), err)) + } +} + +// Parse the command line args. Returns any extra args not accounted for by +// parsing, as well as an error if the parsing returns an error. +func (o *ToolOptions) Parse() ([]string, error) { + return o.parser.Parse() +} + +func (opts *ToolOptions) handleUnknownOption(option string, arg flags.SplitArgument, args []string) ([]string, error) { + if option == "dbpath" || option == "directoryperdb" || option == "journal" { + return args, fmt.Errorf("--dbpath and related flags are not supported in 3.0 tools.\n" + + "See http://dochub.mongodb.org/core/tools-dbpath-deprecated for more information") + } + + return args, fmt.Errorf(`unknown option "%v"`, option) +} + +// getIntArg returns 3 args: the parsed int value, a bool set to true if a value +// was consumed from the incoming args array during parsing, and an error +// value if parsing failed +func getIntArg(arg flags.SplitArgument, args []string) (int, bool, error) { + var rawVal string + consumeValue := false + rawVal, hasVal := arg.Value() + if !hasVal { + if len(args) == 0 { + return 0, false, fmt.Errorf("no value specified") + } + rawVal = args[0] + consumeValue = true + } + val, err := strconv.Atoi(rawVal) + if err != nil { + return val, consumeValue, fmt.Errorf("expected an integer value but got '%v'", rawVal) + } + return val, consumeValue, nil +} + +// getStringArg returns 3 args: the parsed string value, a bool set to true if a value +// was consumed from the incoming args array during parsing, and an error +// value if parsing failed +func getStringArg(arg flags.SplitArgument, args []string) (string, bool, error) { + value, hasVal := arg.Value() + if hasVal { + return value, false, nil + } + if len(args) == 0 { + return "", false, fmt.Errorf("no value specified") + } + return args[0], true, nil +} diff --git a/src/mongo/gotools/common/options/options_gssapi.go b/src/mongo/gotools/common/options/options_gssapi.go new file mode 100644 index 00000000000..f10aa3a2b3b --- /dev/null +++ b/src/mongo/gotools/common/options/options_gssapi.go @@ -0,0 +1,12 @@ +// +build sasl + +package options + +func init() { + ConnectionOptFunctions = append(ConnectionOptFunctions, registerGSSAPIOptions) +} + +func registerGSSAPIOptions(self *ToolOptions) error { + _, err := self.parser.AddGroup("kerberos options", "", self.Kerberos) + return err +} diff --git a/src/mongo/gotools/common/options/options_ssl.go b/src/mongo/gotools/common/options/options_ssl.go new file mode 100644 index 00000000000..e8b8f27171f --- /dev/null +++ b/src/mongo/gotools/common/options/options_ssl.go @@ -0,0 +1,18 @@ +// +build ssl + +package options + +import "github.com/spacemonkeygo/openssl" + +func init() { + ConnectionOptFunctions = append(ConnectionOptFunctions, registerSSLOptions) + versionInfos = append(versionInfos, versionInfo{ + key: "OpenSSL version", + value: openssl.Version, + }) +} + +func registerSSLOptions(self *ToolOptions) error { + _, err := self.parser.AddGroup("ssl options", "", self.SSL) + return err +} diff --git a/src/mongo/gotools/common/options/options_test.go b/src/mongo/gotools/common/options/options_test.go new file mode 100644 index 00000000000..cc18af21506 --- /dev/null +++ b/src/mongo/gotools/common/options/options_test.go @@ -0,0 +1,75 @@ +package options + +import ( + . "github.com/smartystreets/goconvey/convey" + "testing" +) + +func TestVerbosityFlag(t *testing.T) { + Convey("With a new ToolOptions", t, func() { + enabled := EnabledOptions{false, false, false} + optPtr := New("", "", enabled) + So(optPtr, ShouldNotBeNil) + So(optPtr.parser, ShouldNotBeNil) + + Convey("no verbosity flags, Level should be 0", func() { + _, err := optPtr.parser.ParseArgs([]string{}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 0) + }) + + Convey("one short verbosity flag, Level should be 1", func() { + _, err := optPtr.parser.ParseArgs([]string{"-v"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 1) + }) + + Convey("three short verbosity flags (consecutive), Level should be 3", func() { + _, err := optPtr.parser.ParseArgs([]string{"-vvv"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("three short verbosity flags (dispersed), Level should be 3", func() { + _, err := optPtr.parser.ParseArgs([]string{"-v", "-v", "-v"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("short verbosity flag assigned to 3, Level should be 3", func() { + _, err := optPtr.parser.ParseArgs([]string{"-v=3"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("consecutive short flags with assignment, only assignment holds", func() { + _, err := optPtr.parser.ParseArgs([]string{"-vv=3"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("one long verbose flag, Level should be 1", func() { + _, err := optPtr.parser.ParseArgs([]string{"--verbose"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 1) + }) + + Convey("three long verbosity flags, Level should be 3", func() { + _, err := optPtr.parser.ParseArgs([]string{"--verbose", "--verbose", "--verbose"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("long verbosity flag assigned to 3, Level should be 3", func() { + _, err := optPtr.parser.ParseArgs([]string{"--verbose=3"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 3) + }) + + Convey("mixed assignment and bare flag, total is sum", func() { + _, err := optPtr.parser.ParseArgs([]string{"--verbose", "--verbose=3"}) + So(err, ShouldBeNil) + So(optPtr.Level(), ShouldEqual, 4) + }) + }) +} |