16435: Merge branch 'master' into 16435-sync-groups-perm-levels
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 21 May 2020 23:09:38 +0000 (20:09 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Thu, 21 May 2020 23:09:38 +0000 (20:09 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

17 files changed:
apps/workbench/test/integration/anonymous_access_test.rb
apps/workbench/test/integration/collections_test.rb
build/run-build-packages-one-target.sh
build/run-tests.sh
doc/admin/upgrading.html.textile.liquid
doc/install/configure-s3-object-storage.html.textile.liquid
lib/config/config.default.yml
lib/config/generated_config.go
lib/install/deps.go
sdk/go/arvados/config.go
sdk/python/arvados/keep.py
sdk/python/arvados/util.py
services/api/config/arvados_config.rb
services/keepstore/s3_volume.go
services/keepstore/s3_volume_test.go
services/keepstore/unix_volume.go
services/keepstore/unix_volume_test.go

index 0842635f603ff00ad93dbb15581950f860c37567..cbbe28a6f3d1eb2f61ca6a8d11e815aa0702fb3f 100644 (file)
@@ -117,6 +117,7 @@ class AnonymousAccessTest < ActionDispatch::IntegrationTest
   end
 
   test 'view file' do
+    need_selenium "phantomjs does not follow redirects reliably, maybe https://github.com/ariya/phantomjs/issues/10389"
     magic = rand(2**512).to_s 36
     owner = api_fixture('groups')['anonymously_accessible_project']['uuid']
     col = upload_data_and_get_collection(magic, 'admin', "Hello\\040world.txt", owner)
index 87d3d678d174c99e03f527c58a6970f05c122f11..e7b27fff86377c4013a216bc897bf1cc7016b7f4 100644 (file)
@@ -53,6 +53,8 @@ class CollectionsTest < ActionDispatch::IntegrationTest
   end
 
   test "can download an entire collection with a reader token" do
+    need_selenium "phantomjs does not follow redirects reliably, maybe https://github.com/ariya/phantomjs/issues/10389"
+
     token = api_token('active')
     data = "foo\nfile\n"
     datablock = `echo -n #{data.shellescape} | ARVADOS_API_TOKEN=#{token.shellescape} arv-put --no-progress --raw -`.strip
@@ -68,24 +70,16 @@ class CollectionsTest < ActionDispatch::IntegrationTest
     token = api_fixture('api_client_authorizations')['active_all_collections']['api_token']
     url_head = "/collections/download/#{uuid}/#{token}/"
     visit url_head
+    assert_text "You can download individual files listed below"
     # It seems that Capybara can't inspect tags outside the body, so this is
     # a very blunt approach.
     assert_no_match(/<\s*meta[^>]+\bnofollow\b/i, page.html,
                     "wget prohibited from recursing the collection page")
     # Look at all the links that wget would recurse through using our
     # recommended options, and check that it's exactly the file list.
-    hrefs = page.all('a').map do |anchor|
-      link = anchor[:href] || ''
-      if link.start_with? url_head
-        link[url_head.size .. -1]
-      elsif link.start_with? '/'
-        nil
-      else
-        link
-      end
-    end
-    assert_equal(['./foo'], hrefs.compact.sort,
-                 "download page did provide strictly file links")
+    hrefs = []
+    page.html.scan(/href="(.*?)"/) { |m| hrefs << m[0] }
+    assert_equal(['./foo'], hrefs, "download page did provide strictly file links")
     click_link "foo"
     assert_text "foo\nfile\n"
   end
index f476a9691cfb70b8b21ca3fd6a2ae2dd2e051dc7..1a845d200a38c53aeaf3f353e279cf7289a01179 100755 (executable)
@@ -143,6 +143,22 @@ if [[ -n "$test_packages" ]]; then
   fi
 
   if [[ -n "$(find $WORKSPACE/packages/$TARGET -name '*.deb')" ]] ; then
+    set +e
+    /usr/bin/which dpkg-scanpackages >/dev/null
+    if [[ "$?" != "0" ]]; then
+      echo >&2
+      echo >&2 "Error: please install dpkg-dev. E.g. sudo apt-get install dpkg-dev"
+      echo >&2
+      exit 1
+    fi
+    /usr/bin/which apt-ftparchive >/dev/null
+    if [[ "$?" != "0" ]]; then
+      echo >&2
+      echo >&2 "Error: please install apt-utils. E.g. sudo apt-get install apt-utils"
+      echo >&2
+      exit 1
+    fi
+    set -e
     (cd $WORKSPACE/packages/$TARGET
       dpkg-scanpackages .  2> >(grep -v 'warning' 1>&2) | tee Packages | gzip -c > Packages.gz
       apt-ftparchive -o APT::FTPArchive::Release::Origin=Arvados release . > Release
index 0212d1bc0e13e7b6202a04f4da00436a6c278ed1..ff6ead0facc26bbb0e1141d118b4cd81a70ec4c0 100755 (executable)
@@ -1205,6 +1205,7 @@ help_interactive() {
     echo "== Interactive commands:"
     echo "TARGET                 (short for 'test DIR')"
     echo "test TARGET"
+    echo "10 test TARGET         (run test 10 times)"
     echo "test TARGET:py3        (test with python3)"
     echo "test TARGET -check.vv  (pass arguments to test)"
     echo "install TARGET"
@@ -1265,6 +1266,10 @@ else
     while read -p 'What next? ' -e -i "$nextcmd" nextcmd; do
         history -s "$nextcmd"
         history -w
+        count=1
+        if [[ "${nextcmd}" =~ ^[0-9] ]]; then
+          read count nextcmd <<<"${nextcmd}"
+        fi
         read verb target opts <<<"${nextcmd}"
         target="${target%/}"
         target="${target/\/:/:}"
@@ -1284,11 +1289,14 @@ else
                         ${verb}_${target}
                         ;;
                     *)
-                       argstarget=${target%:py3}
+                        argstarget=${target%:py3}
                         testargs["$argstarget"]="${opts}"
                         tt="${testfuncargs[${target}]}"
                         tt="${tt:-$target}"
-                        do_$verb $tt
+                        while [ $count -gt 0 ]; do
+                          do_$verb $tt
+                          let "count=count-1"
+                        done
                         ;;
                 esac
                 ;;
index 070e58983a50fc01c8943d6d29aa46b3d0453361..edd92fa0ea1a117a91d60f3453dc6c3bd146ca3d 100644 (file)
@@ -44,6 +44,10 @@ The SSO (single sign-on) component is deprecated and will not be supported in fu
 
 After migrating your configuration, uninstall the @arvados-sso-provider@ package.
 
+h3. S3 signatures
+
+Keepstore now uses "V4 signatures":https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html by default for S3 requests. If you are using Amazon S3, no action is needed; all regions support V4 signatures. If you are using a different S3-compatible service that does not support V4 signatures, add @V2Signature: true@ to your volume driver parameters to preserve the old behavior. See "configuring S3 object storage":{{site.baseurl}}/install/configure-s3-object-storage.html.
+
 h2(#v2_0_0). v2.0.0 (2020-02-07)
 
 "Upgrading from 1.4":#v1_4_1
index e953f660fbc0defa81bc13ca34ab2138f4f7dc08..b960ac1fda0c2ab1fbaae77e4ae3c875b8dec0bc 100644 (file)
@@ -59,6 +59,11 @@ Volumes are configured in the @Volumes@ section of the cluster configuration fil
           # declaration.
           LocationConstraint: false
 
+          # Use V2 signatures instead of the default V4. Amazon S3
+          # supports V4 signatures in all regions, but this option
+          # might be needed for other S3-compatible services.
+          V2Signature: false
+
           # Requested page size for "list bucket contents" requests.
           IndexPageSize: 1000
 
index 0efe49c1cb9331621ebb6d141ef55697c52464fe..204f7538bad5fc268d191f9fa58a38aa5f7389a0 100644 (file)
@@ -1020,6 +1020,7 @@ Clusters:
           Region: us-east-1a
           Bucket: aaaaa
           LocationConstraint: false
+          V2Signature: false
           IndexPageSize: 1000
           ConnectTimeout: 1m
           ReadTimeout: 10m
index c9d29f814ddddc6d3d443006b823199d2f3e9e93..ec5bc187d7625d504918d8feb8e23abf16e7018c 100644 (file)
@@ -1026,6 +1026,7 @@ Clusters:
           Region: us-east-1a
           Bucket: aaaaa
           LocationConstraint: false
+          V2Signature: false
           IndexPageSize: 1000
           ConnectTimeout: 1m
           ReadTimeout: 10m
index 4e1dc73746a17e20a4e893b5aa877b2d681d0f78..ba57c20c357baeab68d92a5e41d52f8dc208606f 100644 (file)
@@ -311,19 +311,12 @@ rm ${zip}
                        }
                        defer func() {
                                cmd.Process.Signal(syscall.SIGTERM)
-                               logger.Infof("sent SIGTERM; waiting for postgres to shut down")
+                               logger.Info("sent SIGTERM; waiting for postgres to shut down")
                                cmd.Wait()
                        }()
-                       for deadline := time.Now().Add(10 * time.Second); ; {
-                               output, err2 := exec.Command("pg_isready").CombinedOutput()
-                               if err2 == nil {
-                                       break
-                               } else if time.Now().After(deadline) {
-                                       err = fmt.Errorf("timed out waiting for pg_isready (%q)", output)
-                                       return 1
-                               } else {
-                                       time.Sleep(time.Second)
-                               }
+                       err = waitPostgreSQLReady()
+                       if err != nil {
+                               return 1
                        }
                }
 
@@ -334,6 +327,51 @@ rm ${zip}
                        // might never have been run.
                }
 
+               var needcoll []string
+               // If the en_US.UTF-8 locale wasn't installed when
+               // postgresql initdb ran, it needs to be added
+               // explicitly before we can use it in our test suite.
+               for _, collname := range []string{"en_US", "en_US.UTF-8"} {
+                       cmd := exec.Command("sudo", "-u", "postgres", "psql", "-t", "-c", "SELECT 1 FROM pg_catalog.pg_collation WHERE collname='"+collname+"' AND collcollate IN ('en_US.UTF-8', 'en_US.utf8')")
+                       cmd.Dir = "/"
+                       out, err2 := cmd.CombinedOutput()
+                       if err != nil {
+                               err = fmt.Errorf("error while checking postgresql collations: %s", err2)
+                               return 1
+                       }
+                       if strings.Contains(string(out), "1") {
+                               logger.Infof("postgresql supports collation %s", collname)
+                       } else {
+                               needcoll = append(needcoll, collname)
+                       }
+               }
+               if len(needcoll) > 0 && os.Getpid() != 1 {
+                       // In order for the CREATE COLLATION statement
+                       // below to work, the locale must have existed
+                       // when PostgreSQL started up. If we're
+                       // running as init, we must have started
+                       // PostgreSQL ourselves after installing the
+                       // locales. Otherwise, it might need a
+                       // restart, so we attempt to restart it with
+                       // systemd.
+                       if err = runBash(`sudo systemctl restart postgresql`, stdout, stderr); err != nil {
+                               logger.Warn("`systemctl restart postgresql` failed; hoping postgresql does not need to be restarted")
+                       } else if err = waitPostgreSQLReady(); err != nil {
+                               return 1
+                       }
+               }
+               for _, collname := range needcoll {
+                       cmd := exec.Command("sudo", "-u", "postgres", "psql", "-c", "CREATE COLLATION \""+collname+"\" (LOCALE = \"en_US.UTF-8\")")
+                       cmd.Stdout = stdout
+                       cmd.Stderr = stderr
+                       cmd.Dir = "/"
+                       err = cmd.Run()
+                       if err != nil {
+                               err = fmt.Errorf("error adding postgresql collation %s: %s", collname, err)
+                               return 1
+                       }
+               }
+
                withstuff := "WITH LOGIN SUPERUSER ENCRYPTED PASSWORD " + pq.QuoteLiteral(devtestDatabasePassword)
                cmd := exec.Command("sudo", "-u", "postgres", "psql", "-c", "ALTER ROLE arvados "+withstuff)
                cmd.Dir = "/"
@@ -408,6 +446,19 @@ func identifyOS() (osversion, error) {
        return osv, nil
 }
 
+func waitPostgreSQLReady() error {
+       for deadline := time.Now().Add(10 * time.Second); ; {
+               output, err := exec.Command("pg_isready").CombinedOutput()
+               if err == nil {
+                       return nil
+               } else if time.Now().After(deadline) {
+                       return fmt.Errorf("timed out waiting for pg_isready (%q)", output)
+               } else {
+                       time.Sleep(time.Second)
+               }
+       }
+}
+
 func runBash(script string, stdout, stderr io.Writer) error {
        cmd := exec.Command("bash", "-")
        cmd.Stdin = bytes.NewBufferString("set -ex -o pipefail\n" + script)
index 9f9f00e6445ec676b7ca19877cef1e7b304912e2..1efc87ea72ac6f67496e0b4df931905092f2c6fa 100644 (file)
@@ -259,12 +259,14 @@ type Volume struct {
 }
 
 type S3VolumeDriverParameters struct {
+       IAMRole            string
        AccessKey          string
        SecretKey          string
        Endpoint           string
        Region             string
        Bucket             string
        LocationConstraint bool
+       V2Signature        bool
        IndexPageSize      int
        ConnectTimeout     Duration
        ReadTimeout        Duration
index 86a28f54c402c8d44aba1d8511faab18e5e8b44a..bc43b849c3a01dd661c4ba080d83f65f597adde6 100644 (file)
@@ -375,6 +375,8 @@ class KeepClient(object):
                     curl.setopt(pycurl.HEADERFUNCTION, self._headerfunction)
                     if self.insecure:
                         curl.setopt(pycurl.SSL_VERIFYPEER, 0)
+                    else:
+                        curl.setopt(pycurl.CAINFO, arvados.util.ca_certs_path())
                     if method == "HEAD":
                         curl.setopt(pycurl.NOBODY, True)
                     self._setcurltimeouts(curl, timeout, method=="HEAD")
@@ -473,6 +475,8 @@ class KeepClient(object):
                     curl.setopt(pycurl.HEADERFUNCTION, self._headerfunction)
                     if self.insecure:
                         curl.setopt(pycurl.SSL_VERIFYPEER, 0)
+                    else:
+                        curl.setopt(pycurl.CAINFO, arvados.util.ca_certs_path())
                     self._setcurltimeouts(curl, timeout)
                     try:
                         curl.perform()
index dcc0417c138484b9d3f589ea19f75dacf4be6122..6c9822e9f0325ec82cf68dc413843a9499755942 100644 (file)
@@ -396,6 +396,9 @@ def ca_certs_path(fallback=httplib2.CA_CERTS):
     it returns the value of `fallback` (httplib2's CA certs by default).
     """
     for ca_certs_path in [
+        # SSL_CERT_FILE and SSL_CERT_DIR are openssl overrides - note
+        # that httplib2 itself also supports HTTPLIB2_CA_CERTS.
+        os.environ.get('SSL_CERT_FILE'),
         # Arvados specific:
         '/etc/arvados/ca-certificates.crt',
         # Debian:
@@ -403,7 +406,7 @@ def ca_certs_path(fallback=httplib2.CA_CERTS):
         # Red Hat:
         '/etc/pki/tls/certs/ca-bundle.crt',
         ]:
-        if os.path.exists(ca_certs_path):
+        if ca_certs_path and os.path.exists(ca_certs_path):
             return ca_certs_path
     return fallback
 
index 7dc6481008ae8e9d0b9b168c117c68decabf49a1..f63f8af0335884c606ba2c52117d939657b4ff1e 100644 (file)
@@ -190,6 +190,7 @@ dbcfg.declare_config "PostgreSQL.Connection.password", String, :password
 dbcfg.declare_config "PostgreSQL.Connection.dbname", String, :database
 dbcfg.declare_config "PostgreSQL.Connection.template", String, :template
 dbcfg.declare_config "PostgreSQL.Connection.encoding", String, :encoding
+dbcfg.declare_config "PostgreSQL.Connection.collation", String, :collation
 
 application_config = {}
 %w(application.default application).each do |cfgfile|
@@ -257,6 +258,8 @@ if ::Rails.env.to_s == "test"
   # Use template0 when creating a new database. Avoids
   # character-encoding/collation problems.
   $arvados_config["PostgreSQL"]["Connection"]["template"] = "template0"
+  # Some test cases depend on en_US.UTF-8 collation.
+  $arvados_config["PostgreSQL"]["Connection"]["collation"] = "en_US.UTF-8"
 end
 
 if $arvados_config["PostgreSQL"]["Connection"]["password"].empty?
@@ -279,6 +282,7 @@ ENV["DATABASE_URL"] = "postgresql://#{$arvados_config["PostgreSQL"]["Connection"
                       "#{dbhost}/#{$arvados_config["PostgreSQL"]["Connection"]["dbname"]}?"+
                       "template=#{$arvados_config["PostgreSQL"]["Connection"]["template"]}&"+
                       "encoding=#{$arvados_config["PostgreSQL"]["Connection"]["client_encoding"]}&"+
+                      "collation=#{$arvados_config["PostgreSQL"]["Connection"]["collation"]}&"+
                       "pool=#{$arvados_config["PostgreSQL"]["ConnectionPool"]}"
 
 Server::Application.configure do
index 80aa5ec3bb8fe13fe449f8069afc5e0d306d9b11..96f2e7db3965704570f3906c78ab6e624072e013 100644 (file)
@@ -129,20 +129,9 @@ func s3regions() (okList []string) {
 
 // S3Volume implements Volume using an S3 bucket.
 type S3Volume struct {
-       AccessKey          string
-       SecretKey          string
-       AuthToken          string    // populated automatically when IAMRole is used
-       AuthExpiration     time.Time // populated automatically when IAMRole is used
-       IAMRole            string
-       Endpoint           string
-       Region             string
-       Bucket             string
-       LocationConstraint bool
-       IndexPageSize      int
-       ConnectTimeout     arvados.Duration
-       ReadTimeout        arvados.Duration
-       RaceWindow         arvados.Duration
-       UnsafeDelete       bool
+       arvados.S3VolumeDriverParameters
+       AuthToken      string    // populated automatically when IAMRole is used
+       AuthExpiration time.Time // populated automatically when IAMRole is used
 
        cluster   *arvados.Cluster
        volume    arvados.Volume
@@ -188,8 +177,7 @@ func (v *S3Volume) bootstrapIAMCredentials() error {
 func (v *S3Volume) newS3Client() *s3.S3 {
        auth := aws.NewAuth(v.AccessKey, v.SecretKey, v.AuthToken, v.AuthExpiration)
        client := s3.New(*auth, v.region)
-       if v.region.EC2Endpoint.Signer == aws.V4Signature {
-               // Currently affects only eu-central-1
+       if !v.V2Signature {
                client.Signature = aws.V4Signature
        }
        client.ConnectTimeout = time.Duration(v.ConnectTimeout)
index 2c5cdf5b99fa3255d03626933d280ac2e7e21a8a..2736f00b743c791502f78886e716b521a0585eb1 100644 (file)
@@ -101,6 +101,53 @@ func (s *StubbedS3Suite) TestIndex(c *check.C) {
        }
 }
 
+func (s *StubbedS3Suite) TestSignatureVersion(c *check.C) {
+       var header http.Header
+       stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               header = r.Header
+       }))
+       defer stub.Close()
+
+       // Default V4 signature
+       vol := S3Volume{
+               S3VolumeDriverParameters: arvados.S3VolumeDriverParameters{
+                       AccessKey: "xxx",
+                       SecretKey: "xxx",
+                       Endpoint:  stub.URL,
+                       Region:    "test-region-1",
+                       Bucket:    "test-bucket-name",
+               },
+               cluster: s.cluster,
+               logger:  ctxlog.TestLogger(c),
+               metrics: newVolumeMetricsVecs(prometheus.NewRegistry()),
+       }
+       err := vol.check()
+       c.Check(err, check.IsNil)
+       err = vol.Put(context.Background(), "acbd18db4cc2f85cedef654fccc4a4d8", []byte("foo"))
+       c.Check(err, check.IsNil)
+       c.Check(header.Get("Authorization"), check.Matches, `AWS4-HMAC-SHA256 .*`)
+
+       // Force V2 signature
+       vol = S3Volume{
+               S3VolumeDriverParameters: arvados.S3VolumeDriverParameters{
+                       AccessKey:   "xxx",
+                       SecretKey:   "xxx",
+                       Endpoint:    stub.URL,
+                       Region:      "test-region-1",
+                       Bucket:      "test-bucket-name",
+                       V2Signature: true,
+               },
+               cluster: s.cluster,
+               logger:  ctxlog.TestLogger(c),
+               metrics: newVolumeMetricsVecs(prometheus.NewRegistry()),
+       }
+       err = vol.check()
+       c.Check(err, check.IsNil)
+       err = vol.Put(context.Background(), "acbd18db4cc2f85cedef654fccc4a4d8", []byte("foo"))
+       c.Check(err, check.IsNil)
+       c.Check(header.Get("Authorization"), check.Matches, `AWS xxx:.*`)
+}
+
 func (s *StubbedS3Suite) TestIAMRoleCredentials(c *check.C) {
        s.metadata = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                upd := time.Now().UTC().Add(-time.Hour).Format(time.RFC3339)
@@ -122,13 +169,15 @@ func (s *StubbedS3Suite) TestIAMRoleCredentials(c *check.C) {
                w.WriteHeader(http.StatusNotFound)
        }))
        deadv := &S3Volume{
-               IAMRole:  s.metadata.URL + "/fake-metadata/test-role",
-               Endpoint: "http://localhost:12345",
-               Region:   "test-region-1",
-               Bucket:   "test-bucket-name",
-               cluster:  s.cluster,
-               logger:   ctxlog.TestLogger(c),
-               metrics:  newVolumeMetricsVecs(prometheus.NewRegistry()),
+               S3VolumeDriverParameters: arvados.S3VolumeDriverParameters{
+                       IAMRole:  s.metadata.URL + "/fake-metadata/test-role",
+                       Endpoint: "http://localhost:12345",
+                       Region:   "test-region-1",
+                       Bucket:   "test-bucket-name",
+               },
+               cluster: s.cluster,
+               logger:  ctxlog.TestLogger(c),
+               metrics: newVolumeMetricsVecs(prometheus.NewRegistry()),
        }
        err := deadv.check()
        c.Check(err, check.ErrorMatches, `.*/fake-metadata/test-role.*`)
@@ -468,19 +517,21 @@ func (s *StubbedS3Suite) newTestableVolume(c *check.C, cluster *arvados.Cluster,
 
        v := &TestableS3Volume{
                S3Volume: &S3Volume{
-                       AccessKey:          accessKey,
-                       SecretKey:          secretKey,
-                       IAMRole:            iamRole,
-                       Bucket:             TestBucketName,
-                       Endpoint:           endpoint,
-                       Region:             "test-region-1",
-                       LocationConstraint: true,
-                       UnsafeDelete:       true,
-                       IndexPageSize:      1000,
-                       cluster:            cluster,
-                       volume:             volume,
-                       logger:             ctxlog.TestLogger(c),
-                       metrics:            metrics,
+                       S3VolumeDriverParameters: arvados.S3VolumeDriverParameters{
+                               IAMRole:            iamRole,
+                               AccessKey:          accessKey,
+                               SecretKey:          secretKey,
+                               Bucket:             TestBucketName,
+                               Endpoint:           endpoint,
+                               Region:             "test-region-1",
+                               LocationConstraint: true,
+                               UnsafeDelete:       true,
+                               IndexPageSize:      1000,
+                       },
+                       cluster: cluster,
+                       volume:  volume,
+                       logger:  ctxlog.TestLogger(c),
+                       metrics: metrics,
                },
                c:           c,
                server:      srv,
index ceccd11c92172a0f018ec87f25a95bfdada2bf33..5026e2d32558e085886ba119cf0b664bfbc58473 100644 (file)
@@ -172,10 +172,10 @@ func (v *UnixVolume) Touch(loc string) error {
                return e
        }
        defer v.unlockfile(f)
-       ts := syscall.NsecToTimespec(time.Now().UnixNano())
+       ts := time.Now()
        v.os.stats.TickOps("utimes")
        v.os.stats.Tick(&v.os.stats.UtimesOps)
-       err = syscall.UtimesNano(p, []syscall.Timespec{ts, ts})
+       err = os.Chtimes(p, ts, ts)
        v.os.stats.TickErr(err)
        return err
 }
@@ -298,6 +298,19 @@ func (v *UnixVolume) WriteBlock(ctx context.Context, loc string, rdr io.Reader)
                v.os.Remove(tmpfile.Name())
                return err
        }
+       // ext4 uses a low-precision clock and effectively backdates
+       // files by up to 10 ms, sometimes across a 1-second boundary,
+       // which produces confusing results in logs and tests.  We
+       // avoid this by setting the output file's timestamps
+       // explicitly, using a higher resolution clock.
+       ts := time.Now()
+       v.os.stats.TickOps("utimes")
+       v.os.stats.Tick(&v.os.stats.UtimesOps)
+       if err = os.Chtimes(tmpfile.Name(), ts, ts); err != nil {
+               err = fmt.Errorf("error setting timestamps on %s: %s", tmpfile.Name(), err)
+               v.os.Remove(tmpfile.Name())
+               return err
+       }
        if err := v.os.Rename(tmpfile.Name(), bpath); err != nil {
                err = fmt.Errorf("error renaming %s to %s: %s", tmpfile.Name(), bpath, err)
                v.os.Remove(tmpfile.Name())
index 7777363b9d13815ab3036ae916a2c0f6989eb95f..5a3a536944daa5b8012bc0b2afbf8b6932862364 100644 (file)
@@ -405,13 +405,13 @@ func (s *UnixVolumeSuite) TestStats(c *check.C) {
        c.Check(stats(), check.Matches, `.*"OutBytes":3,.*`)
        c.Check(stats(), check.Matches, `.*"CreateOps":1,.*`)
        c.Check(stats(), check.Matches, `.*"OpenOps":0,.*`)
-       c.Check(stats(), check.Matches, `.*"UtimesOps":0,.*`)
+       c.Check(stats(), check.Matches, `.*"UtimesOps":1,.*`)
 
        err = vol.Touch(loc)
        c.Check(err, check.IsNil)
        c.Check(stats(), check.Matches, `.*"FlockOps":1,.*`)
        c.Check(stats(), check.Matches, `.*"OpenOps":1,.*`)
-       c.Check(stats(), check.Matches, `.*"UtimesOps":1,.*`)
+       c.Check(stats(), check.Matches, `.*"UtimesOps":2,.*`)
 
        _, err = vol.Get(context.Background(), loc, make([]byte, 3))
        c.Check(err, check.IsNil)