diff --git a/flags.go b/flags.go index 9fb6fff55c..2bc6f6cf25 100644 --- a/flags.go +++ b/flags.go @@ -31,40 +31,59 @@ func newApp() (app *cli.App) { HideVersion: true, Writer: os.Stderr, Flags: []cli.Flag{ + + cli.BoolFlag{ + Name: "help, h", + Usage: "Print this help text and exit successfuly.", + }, + + ///////////////////////// + // File system + ///////////////////////// + + cli.StringSliceFlag{ + Name: "o", + Usage: "Additional system-specific mount options. Be careful!", + }, + cli.IntFlag{ Name: "dir-mode", Value: 0755, Usage: "Permissions bits for directories. (default: 0755)", HideDefault: true, }, + cli.IntFlag{ Name: "file-mode", Value: 0644, Usage: "Permission bits for files (default: 0644)", HideDefault: true, }, + cli.IntFlag{ - Name: "gcs-chunk-size", - Value: 1 << 24, - Usage: "Max chunk size for loading GCS objects.", + Name: "uid", + Value: -1, + HideDefault: true, + Usage: "UID owner of all inodes.", }, + cli.IntFlag{ Name: "gid", Value: -1, HideDefault: true, Usage: "GID owner of all inodes.", }, + cli.BoolFlag{ Name: "implicit-dirs", Usage: "Implicitly define directories based on content. See" + "docs/semantics.md", }, - cli.Float64Flag{ - Name: "limit-bytes-per-sec", - Value: -1, - Usage: "Bandwidth limit for reading data, measured over a 30-second " + - "window. (use -1 for no limit)", - }, + + ///////////////////////// + // GCS + ///////////////////////// + cli.StringFlag{ Name: "key-file", Value: "", @@ -72,28 +91,44 @@ func newApp() (app *cli.App) { Usage: "Path to JSON key file for use with GCS. " + "(default: none, Google application default credentials used)", }, + + cli.Float64Flag{ + Name: "limit-bytes-per-sec", + Value: -1, + Usage: "Bandwidth limit for reading data, measured over a 30-second " + + "window. (use -1 for no limit)", + }, + cli.Float64Flag{ Name: "limit-ops-per-sec", Value: 5.0, Usage: "Operations per second limit, measured over a 30-second window " + "(use -1 for no limit)", }, - cli.StringFlag{ - Name: "mount-options, o", - HideDefault: true, - Usage: "Additional system-specific mount options. Be careful!", - }, + + ///////////////////////// + // Tuning + ///////////////////////// + cli.DurationFlag{ Name: "stat-cache-ttl", Value: time.Minute, Usage: "How long to cache StatObject results from GCS.", }, + cli.DurationFlag{ Name: "type-cache-ttl", Value: time.Minute, Usage: "How long to cache name -> file/dir mappings in directory " + "inodes.", }, + + cli.IntFlag{ + Name: "gcs-chunk-size", + Value: 1 << 24, + Usage: "Max chunk size for loading GCS objects.", + }, + cli.StringFlag{ Name: "temp-dir", Value: "", @@ -101,37 +136,36 @@ func newApp() (app *cli.App) { Usage: "Temporary directory for local GCS object copies. " + "(default: system default, likely /tmp)", }, + cli.IntFlag{ Name: "temp-dir-bytes", Value: 1 << 31, Usage: "Size limit of the temporary directory.", }, - cli.IntFlag{ - Name: "uid", - Value: -1, - HideDefault: true, - Usage: "UID owner of all inodes.", - }, + + ///////////////////////// + // Debugging + ///////////////////////// + cli.BoolFlag{ Name: "debug_fuse", Usage: "Enable fuse-related debugging output.", }, + cli.BoolFlag{ Name: "debug_gcs", Usage: "Print GCS request and timing information.", }, + cli.BoolFlag{ Name: "debug_http", Usage: "Dump HTTP requests and responses to/from GCS.", }, + cli.BoolFlag{ Name: "debug_invariants", Usage: "Panic when internal invariants are violated.", }, - cli.BoolFlag{ - Name: "help, h", - Usage: "Print this help text and exit successfuly.", - }, }, } @@ -139,48 +173,68 @@ func newApp() (app *cli.App) { } type flagStorage struct { - MountOptions map[string]string - Uid int64 - Gid int64 - FileMode uint - DirMode uint - TempDir string - TempDirLimit int64 - GCSChunkSize uint64 - ImplicitDirs bool - StatCacheTTL time.Duration - TypeCacheTTL time.Duration - OpRateLimitHz float64 - EgressBandwidthLimitBytesPerSecond float64 + // File system + MountOptions map[string]string + DirMode os.FileMode + FileMode os.FileMode + Uid int64 + Gid int64 + ImplicitDirs bool + + // GCS KeyFile string - DebugFuse bool - DebugGCS bool - DebugHTTP bool - DebugInvariants bool + EgressBandwidthLimitBytesPerSecond float64 + OpRateLimitHz float64 + + // Tuning + StatCacheTTL time.Duration + TypeCacheTTL time.Duration + GCSChunkSize uint64 + TempDir string + TempDirLimit int64 + + // Debugging + DebugFuse bool + DebugGCS bool + DebugHTTP bool + DebugInvariants bool } // Add the flags accepted by run to the supplied flag set, returning the // variables into which the flags will parse. func populateFlags(c *cli.Context) (flags *flagStorage) { - flags = new(flagStorage) - flags.MountOptions = make(map[string]string) - mountpkg.OptionValue(flags.MountOptions).Set(c.String("mount-options")) - flags.Uid = int64(c.Int("uid")) - flags.Gid = int64(c.Int("gid")) - flags.FileMode = uint(c.Int("file-mode")) - flags.DirMode = uint(c.Int("dir-mode")) - flags.TempDir = c.String("temp-dir") - flags.TempDirLimit = int64(c.Int("temp-dir-bytes")) - flags.GCSChunkSize = uint64(c.Int("gcs-chunk-size")) - flags.ImplicitDirs = c.Bool("implicit-dirs") - flags.StatCacheTTL = c.Duration("stat-cache-ttl") - flags.TypeCacheTTL = c.Duration("type-cache-ttl") - flags.OpRateLimitHz = c.Float64("limit-ops-per-sec") - flags.EgressBandwidthLimitBytesPerSecond = c.Float64("limit-bytes-per-sec") - flags.KeyFile = c.String("key-file") - flags.DebugFuse = c.Bool("debug_fuse") - flags.DebugGCS = c.Bool("debug_gcs") - flags.DebugHTTP = c.Bool("debug_http") - flags.DebugInvariants = c.Bool("debug_invariants") + flags = &flagStorage{ + // File system + MountOptions: make(map[string]string), + DirMode: os.FileMode(c.Int("dir-mode")), + FileMode: os.FileMode(c.Int("file-mode")), + Uid: int64(c.Int("uid")), + Gid: int64(c.Int("gid")), + + // GCS, + KeyFile: c.String("key-file"), + EgressBandwidthLimitBytesPerSecond: c.Float64("limit-bytes-per-sec"), + OpRateLimitHz: c.Float64("limit-ops-per-sec"), + + // Tuning, + StatCacheTTL: c.Duration("stat-cache-ttl"), + TypeCacheTTL: c.Duration("type-cache-ttl"), + GCSChunkSize: uint64(c.Int("gcs-chunk-size")), + TempDir: c.String("temp-dir"), + TempDirLimit: int64(c.Int("temp-dir-bytes")), + ImplicitDirs: c.Bool("implicit-dirs"), + + // Debugging, + DebugFuse: c.Bool("debug_fuse"), + DebugGCS: c.Bool("debug_gcs"), + DebugHTTP: c.Bool("debug_http"), + DebugInvariants: c.Bool("debug_invariants"), + } + + // Handle the repeated "-o" flag. + for _, o := range c.StringSlice("o") { + mountpkg.ParseOptions(flags.MountOptions, o) + } + return } diff --git a/flags_test.go b/flags_test.go new file mode 100644 index 0000000000..d619351477 --- /dev/null +++ b/flags_test.go @@ -0,0 +1,209 @@ +// Copyright 2015 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + "os" + "sort" + "testing" + "time" + + . "github.com/jacobsa/oglematchers" + . "github.com/jacobsa/ogletest" + "github.com/jgeewax/cli" +) + +func TestFlags(t *testing.T) { RunTests(t) } + +//////////////////////////////////////////////////////////////////////// +// Boilerplate +//////////////////////////////////////////////////////////////////////// + +type FlagsTest struct { +} + +func init() { RegisterTestSuite(&FlagsTest{}) } + +func parseArgs(args []string) (flags *flagStorage) { + // Create a CLI app, and abuse it to snoop on the flags. + app := newApp() + app.Action = func(appCtx *cli.Context) { + flags = populateFlags(appCtx) + } + + // Simulate argv. + fullArgs := append([]string{"some_app"}, args...) + + err := app.Run(fullArgs) + AssertEq(nil, err) + + return +} + +//////////////////////////////////////////////////////////////////////// +// Tests +//////////////////////////////////////////////////////////////////////// + +func (t *FlagsTest) Defaults() { + f := parseArgs([]string{}) + + // File system + ExpectNe(nil, f.MountOptions) + ExpectEq(0, len(f.MountOptions), "Options: %v", f.MountOptions) + + ExpectEq(os.FileMode(0755), f.DirMode) + ExpectEq(os.FileMode(0644), f.FileMode) + ExpectEq(-1, f.Uid) + ExpectEq(-1, f.Gid) + ExpectFalse(f.ImplicitDirs) + + // GCS + ExpectEq("", f.KeyFile) + ExpectEq(-1, f.EgressBandwidthLimitBytesPerSecond) + ExpectEq(5, f.OpRateLimitHz) + + // Tuning + ExpectEq(time.Minute, f.StatCacheTTL) + ExpectEq(time.Minute, f.TypeCacheTTL) + ExpectEq(1<<24, f.GCSChunkSize) + ExpectEq("", f.TempDir) + ExpectEq(1<<31, f.TempDirLimit) + + // Debugging + ExpectFalse(f.DebugFuse) + ExpectFalse(f.DebugGCS) + ExpectFalse(f.DebugHTTP) + ExpectFalse(f.DebugInvariants) +} + +func (t *FlagsTest) Bools() { + names := []string{ + "implicit-dirs", + "debug_fuse", + "debug_gcs", + "debug_http", + "debug_invariants", + } + + var args []string + var f *flagStorage + + // --foo form + args = nil + for _, n := range names { + args = append(args, fmt.Sprintf("-%s", n)) + } + + f = parseArgs(args) + ExpectTrue(f.ImplicitDirs) + ExpectTrue(f.DebugFuse) + ExpectTrue(f.DebugGCS) + ExpectTrue(f.DebugHTTP) + ExpectTrue(f.DebugInvariants) + + // --foo=false form + args = nil + for _, n := range names { + args = append(args, fmt.Sprintf("-%s=false", n)) + } + + f = parseArgs(args) + ExpectFalse(f.ImplicitDirs) + ExpectFalse(f.DebugFuse) + ExpectFalse(f.DebugGCS) + ExpectFalse(f.DebugHTTP) + ExpectFalse(f.DebugInvariants) + + // --foo=true form + args = nil + for _, n := range names { + args = append(args, fmt.Sprintf("-%s=true", n)) + } + + f = parseArgs(args) + ExpectTrue(f.ImplicitDirs) + ExpectTrue(f.DebugFuse) + ExpectTrue(f.DebugGCS) + ExpectTrue(f.DebugHTTP) + ExpectTrue(f.DebugInvariants) +} + +func (t *FlagsTest) Numbers() { + args := []string{ + "--dir-mode=0711", + "--file-mode", "0611", + "--uid=17", + "--gid=19", + "--limit-bytes-per-sec=123.4", + "--limit-ops-per-sec=56.78", + "--gcs-chunk-size=1000", + "--temp-dir-bytes=2000", + } + + f := parseArgs(args) + ExpectEq(os.FileMode(0711), f.DirMode) + ExpectEq(os.FileMode(0611), f.FileMode) + ExpectEq(17, f.Uid) + ExpectEq(19, f.Gid) + ExpectEq(123.4, f.EgressBandwidthLimitBytesPerSecond) + ExpectEq(56.78, f.OpRateLimitHz) + ExpectEq(1000, f.GCSChunkSize) + ExpectEq(2000, f.TempDirLimit) +} + +func (t *FlagsTest) Strings() { + args := []string{ + "--key-file", "-asdf", + "--temp-dir=foobar", + } + + f := parseArgs(args) + ExpectEq("-asdf", f.KeyFile) + ExpectEq("foobar", f.TempDir) +} + +func (t *FlagsTest) Durations() { + args := []string{ + "--stat-cache-ttl", "1m17s", + "--type-cache-ttl", "19ns", + } + + f := parseArgs(args) + ExpectEq(77*time.Second, f.StatCacheTTL) + ExpectEq(19*time.Nanosecond, f.TypeCacheTTL) +} + +func (t *FlagsTest) Maps() { + args := []string{ + "-o", "rw,nodev", + "-o", "user=jacobsa,noauto", + } + + f := parseArgs(args) + + var keys sort.StringSlice + for k := range f.MountOptions { + keys = append(keys, k) + } + + sort.Sort(keys) + AssertThat(keys, ElementsAre("noauto", "nodev", "rw", "user")) + + ExpectEq("", f.MountOptions["noauto"]) + ExpectEq("", f.MountOptions["nodev"]) + ExpectEq("", f.MountOptions["rw"]) + ExpectEq("jacobsa", f.MountOptions["user"]) +} diff --git a/gcsfuse_mount_helper/main.go b/gcsfuse_mount_helper/main.go index 2a1df782e2..0480699f50 100644 --- a/gcsfuse_mount_helper/main.go +++ b/gcsfuse_mount_helper/main.go @@ -145,11 +145,7 @@ func parseArgs( // Is this an options string following a "-o"? case i > 0 && args[i-1] == "-o": - err = mount.ParseOptions(opts, s) - if err != nil { - err = fmt.Errorf("ParseOptions(%q): %v", s, err) - return - } + mount.ParseOptions(opts, s) // Is this the device? case positionalCount == 0: diff --git a/mount/flag.go b/mount/flag.go index 735c72b0e0..780ef83924 100644 --- a/mount/flag.go +++ b/mount/flag.go @@ -15,36 +15,7 @@ // Helper functions for dealing with mount(8)-style flags. package mount -import ( - "flag" - "fmt" - "strings" -) - -// Return a flag.Value that can be used to receive options in the format -// accepted by mount(8) and generated for its external mount helpers, using -// ParseOptions to parse and insert into the supplied map. -func OptionValue(m map[string]string) (v flag.Value) { - if m == nil { - panic("m must be non-nil.") - } - - v = &optionFlag{m} - return -} - -type optionFlag struct { - opts map[string]string -} - -func (of *optionFlag) String() string { - return fmt.Sprint(of.opts) -} - -func (of *optionFlag) Set(s string) (err error) { - err = ParseOptions(of.opts, s) - return -} +import "strings" // Parse an option string in the format accepted by mount(8) and generated for // its external mount helpers. @@ -63,7 +34,7 @@ func (of *optionFlag) Set(s string) (err error) { // "foo": "bar=baz", // "qux": "", // -func ParseOptions(m map[string]string, s string) (err error) { +func ParseOptions(m map[string]string, s string) { // NOTE(jacobsa): The man pages don't define how escaping works, and as far // as I can tell there is no way to properly escape or quote a comma in the // options list for an fstab entry. So put our fingers in our ears and hope