Merge branch '17967-storage-classes-config' into main
authorTom Clegg <tom@curii.com>
Tue, 10 Aug 2021 14:43:25 +0000 (10:43 -0400)
committerTom Clegg <tom@curii.com>
Tue, 10 Aug 2021 14:43:25 +0000 (10:43 -0400)
closes #17967

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

23 files changed:
doc/admin/storage-classes.html.textile.liquid
doc/admin/upgrading.html.textile.liquid
doc/user/topics/storage-classes.html.textile.liquid
lib/boot/supervisor.go
lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/config/load.go
lib/config/load_test.go
sdk/go/arvados/config.go
sdk/python/arvados/commands/put.py
services/api/app/models/collection.rb
services/api/app/models/container.rb
services/api/app/models/container_request.rb
services/api/config/arvados_config.rb
services/api/test/unit/collection_test.rb
services/api/test/unit/container_request_test.rb
services/keep-balance/balance.go
services/keep-balance/balance_run_test.go
services/keep-balance/balance_test.go
services/keepstore/handler_test.go
services/keepstore/handlers.go
services/keepstore/volume.go

index 3e17831b58d6d32f21b0c278253aea4394334fac..ade9633879cc3e667ab1fd9fd41d53a668d21cec 100644 (file)
@@ -12,21 +12,44 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 
 Storage classes (alternately known as "storage tiers") allow you to control which volumes should be used to store particular collection data blocks.  This can be used to implement data storage policies such as moving data to archival storage.
 
-The storage classes for each volume are set in the per-volume "keepstore configuration":{{site.baseurl}}/install/install-keepstore.html
+In the default Arvados configuration, with no storage classes specified in the configuration file, all volumes belong to a single implicit storage class called "default". Apart from that, names of storage classes are internal to the cluster and decided by the administrator.  Other than the implicit "default" class, Arvados currently does not define any standard storage class names.
+
+To use multiple storage classes, update the @StorageClasses@ and @Volumes@ sections of your configuration file.
+* Every storage class you use (including "default") must be defined in the @StorageClasses@ section.
+* The @StorageClasses@ section must use @Default: true@ to indicate at least one default storage class. When a client/user does not specify storage classes when creating a new collection, the default storage classes are used implicitly.
+* If some storage classes are faster or cheaper to access than others, assign a higher @Priority@ to the faster ones. When reading data, volumes with high priority storage classes are searched first.
+
+Example:
 
 <pre>
+    StorageClasses:
+
+      default:
+        # When reading a block that is stored on multiple volumes,
+        # prefer a volume with this class.
+        Priority: 20
+
+        # When a client does not specify a storage class when saving a
+        # new collection, use this one.
+        Default: true
+
+      archival:
+        Priority: 10
+
     Volumes:
+
       ClusterID-nyw5e-000000000000000:
         # This volume is in the "default" storage class.
         StorageClasses:
           default: true
+
       ClusterID-nyw5e-000000000000001:
-        # Specify this volume is in the "archival" storage class.
+        # This volume is in the "archival" storage class.
         StorageClasses:
           archival: true
 </pre>
 
-Names of storage classes are internal to the cluster and decided by the administrator.  Aside from "default", Arvados currently does not define any standard storage class names.
+Refer to the "configuration reference":{{site.baseurl}}/admin/config.html for more details.
 
 h3. Using storage classes
 
index dfb6a0ad6e00d9e58cd2fe33bea7f32efc70cadf..b40082deba630f6fa208646b374e9bee311dd0cf 100644 (file)
@@ -39,6 +39,10 @@ h2(#main). development main (as of 2021-07-15)
 
 "Upgrading from 2.2.0":#v2_2_0
 
+h3. Storage classes must be defined explicitly
+
+If your configuration uses the StorageClasses attribute on any Keep volumes, you must add a new @StorageClasses@ section that lists all of your storage classes. Refer to the updated documentation about "configuring storage classes":{{site.baseurl}}/admin/storage-classes.html for details.
+
 h3. keep-balance requires access to PostgreSQL
 
 Make sure the keep-balance process can connect to your PostgreSQL server using the settings in your config file. (In previous versions, keep-balance accessed the database through controller instead of connecting to the database server directly.)
index 99556af10aecfce48a9ac07c37e07959c44e169a..650c3709559546aea019c2348607a30d79246cb2 100644 (file)
@@ -38,7 +38,7 @@ You may also specify the desired storage class for the final output collection p
 $ arvados-cwl-runner --storage-classes=hot myworkflow.cwl myinput.yml
 </pre>
 
-(Note: intermediate collections produced by a workflow run will have "default" storage class.)
+(Note: intermediate collections produced by a workflow run will use the cluster's default storage class(es).)
 
 h3. arv command line
 
@@ -52,8 +52,6 @@ By setting "storage_classes_desired" to "archival", the blocks that make up the
 
 h3. Storage class notes
 
-Collection blocks will be in the "default" storage class if not otherwise specified.
+Collection blocks will be in the cluster's configured default storage class(es) if not otherwise specified.
 
 Any user with write access to a collection may set any storage class on that collection.
-
-Names of storage classes are internal to the cluster and decided by the administrator.  Aside from "default", Arvados currently does not define any standard storage class names.
index 4e009f45ab55ad6353944bbea5cb7ca5b09811ac..2026b8c843fc16d12cc4eef6a0a547f3a1b9b164 100644 (file)
@@ -748,6 +748,11 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
                                },
                        }
                }
+               cluster.StorageClasses = map[string]arvados.StorageClassConfig{
+                       "default": {Default: true},
+                       "foo":     {},
+                       "bar":     {},
+               }
        }
        if super.OwnTemporaryDatabase {
                cluster.PostgreSQL.Connection = arvados.PostgreSQLConnection{
index 66f508b5adf2c3c10f2bca8ce2d72f0e44a0f327..8640e71418fa88c51a7a8c3436949d9371bafee9 100644 (file)
@@ -1228,6 +1228,29 @@ Clusters:
         Price: 0.1
         Preemptible: false
 
+    StorageClasses:
+
+      # If you use multiple storage classes, specify them here, using
+      # the storage class name as the key (in place of "SAMPLE" in
+      # this sample entry).
+      #
+      # Further info/examples:
+      # https://doc.arvados.org/admin/storage-classes.html
+      SAMPLE:
+
+        # Priority determines the order volumes should be searched
+        # when reading data, in cases where a keepstore server has
+        # access to multiple volumes with different storage classes.
+        Priority: 0
+
+        # Default determines which storage class(es) should be used
+        # when a user/client writes data or saves a new collection
+        # without specifying storage classes.
+        #
+        # If any StorageClasses are configured, at least one of them
+        # must have Default: true.
+        Default: true
+
     Volumes:
       SAMPLE:
         # AccessViaHosts specifies which keepstore processes can read
@@ -1251,7 +1274,9 @@ Clusters:
         ReadOnly: false
         Replication: 1
         StorageClasses:
-          default: true
+          # If you have configured storage classes (see StorageClasses
+          # section above), add an entry here for each storage class
+          # satisfied by this volume.
           SAMPLE: true
         Driver: S3
         DriverParameters:
index bbc5ea6c55b885244fc0c33e51a50f36c0f64ca1..2a3d0e173a6d23d36474a55627f6cf73b758df4e 100644 (file)
@@ -205,6 +205,10 @@ var whitelist = map[string]bool{
        "Services.*":                                          true,
        "Services.*.ExternalURL":                              true,
        "Services.*.InternalURLs":                             false,
+       "StorageClasses":                                      true,
+       "StorageClasses.*":                                    true,
+       "StorageClasses.*.Default":                            true,
+       "StorageClasses.*.Priority":                           true,
        "SystemLogs":                                          false,
        "SystemRootToken":                                     false,
        "TLS":                                                 false,
index ee230841354522ad155e25ce794dbca6f549b58f..55e8ba8f36da791007b8ad9ccf755b79733d4698 100644 (file)
@@ -1234,6 +1234,29 @@ Clusters:
         Price: 0.1
         Preemptible: false
 
+    StorageClasses:
+
+      # If you use multiple storage classes, specify them here, using
+      # the storage class name as the key (in place of "SAMPLE" in
+      # this sample entry).
+      #
+      # Further info/examples:
+      # https://doc.arvados.org/admin/storage-classes.html
+      SAMPLE:
+
+        # Priority determines the order volumes should be searched
+        # when reading data, in cases where a keepstore server has
+        # access to multiple volumes with different storage classes.
+        Priority: 0
+
+        # Default determines which storage class(es) should be used
+        # when a user/client writes data or saves a new collection
+        # without specifying storage classes.
+        #
+        # If any StorageClasses are configured, at least one of them
+        # must have Default: true.
+        Default: true
+
     Volumes:
       SAMPLE:
         # AccessViaHosts specifies which keepstore processes can read
@@ -1257,7 +1280,9 @@ Clusters:
         ReadOnly: false
         Replication: 1
         StorageClasses:
-          default: true
+          # If you have configured storage classes (see StorageClasses
+          # section above), add an entry here for each storage class
+          # satisfied by this volume.
           SAMPLE: true
         Driver: S3
         DriverParameters:
index 169b252a0e8fbbe44b0e6722e7fe1fe745ca01b6..248960beb99f875620dca5ee7738f8575877c57d 100644 (file)
@@ -269,6 +269,7 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                        ldr.loadOldKeepBalanceConfig,
                )
        }
+       loadFuncs = append(loadFuncs, ldr.setImplicitStorageClasses)
        for _, f := range loadFuncs {
                err = f(&cfg)
                if err != nil {
@@ -296,6 +297,7 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                        checkKeyConflict(fmt.Sprintf("Clusters.%s.PostgreSQL.Connection", id), cc.PostgreSQL.Connection),
                        ldr.checkEmptyKeepstores(cc),
                        ldr.checkUnlistedKeepstores(cc),
+                       ldr.checkStorageClasses(cc),
                        // TODO: check non-empty Rendezvous on
                        // services other than Keepstore
                } {
@@ -336,6 +338,57 @@ func (ldr *Loader) checkToken(label, token string) error {
        return nil
 }
 
+func (ldr *Loader) setImplicitStorageClasses(cfg *arvados.Config) error {
+cluster:
+       for id, cc := range cfg.Clusters {
+               if len(cc.StorageClasses) > 0 {
+                       continue cluster
+               }
+               for _, vol := range cc.Volumes {
+                       if len(vol.StorageClasses) > 0 {
+                               continue cluster
+                       }
+               }
+               // No explicit StorageClasses config info at all; fill
+               // in implicit defaults.
+               for id, vol := range cc.Volumes {
+                       vol.StorageClasses = map[string]bool{"default": true}
+                       cc.Volumes[id] = vol
+               }
+               cc.StorageClasses = map[string]arvados.StorageClassConfig{"default": {Default: true}}
+               cfg.Clusters[id] = cc
+       }
+       return nil
+}
+
+func (ldr *Loader) checkStorageClasses(cc arvados.Cluster) error {
+       classOnVolume := map[string]bool{}
+       for volid, vol := range cc.Volumes {
+               if len(vol.StorageClasses) == 0 {
+                       return fmt.Errorf("%s: volume has no StorageClasses listed", volid)
+               }
+               for classid := range vol.StorageClasses {
+                       if _, ok := cc.StorageClasses[classid]; !ok {
+                               return fmt.Errorf("%s: volume refers to storage class %q that is not defined in StorageClasses", volid, classid)
+                       }
+                       classOnVolume[classid] = true
+               }
+       }
+       haveDefault := false
+       for classid, sc := range cc.StorageClasses {
+               if !classOnVolume[classid] && len(cc.Volumes) > 0 {
+                       ldr.Logger.Warnf("there are no volumes providing storage class %q", classid)
+               }
+               if sc.Default {
+                       haveDefault = true
+               }
+       }
+       if !haveDefault {
+               return fmt.Errorf("there is no default storage class (at least one entry in StorageClasses must have Default: true)")
+       }
+       return nil
+}
+
 func checkKeyConflict(label string, m map[string]string) error {
        saw := map[string]bool{}
        for k := range m {
index 396faca48461b30d7d5708a55f05bafcf73159ac..d4896c39cb19c2aa782a36389d9d92af728f04f5 100644 (file)
@@ -29,6 +29,8 @@ func Test(t *testing.T) {
 
 var _ = check.Suite(&LoadSuite{})
 
+var emptyConfigYAML = `Clusters: {"z1111": {}}`
+
 // Return a new Loader that reads cluster config from configdata
 // (instead of the usual default /etc/arvados/config.yml), and logs to
 // logdst or (if that's nil) c.Log.
@@ -59,7 +61,7 @@ func (s *LoadSuite) TestEmpty(c *check.C) {
 }
 
 func (s *LoadSuite) TestNoConfigs(c *check.C) {
-       cfg, err := testLoader(c, `Clusters: {"z1111": {}}`, nil).Load()
+       cfg, err := testLoader(c, emptyConfigYAML, nil).Load()
        c.Assert(err, check.IsNil)
        c.Assert(cfg.Clusters, check.HasLen, 1)
        cc, err := cfg.GetCluster("z1111")
@@ -79,7 +81,7 @@ func (s *LoadSuite) TestMungeLegacyConfigArgs(c *check.C) {
        f, err = ioutil.TempFile("", "")
        c.Check(err, check.IsNil)
        defer os.Remove(f.Name())
-       io.WriteString(f, "Clusters: {aaaaa: {}}\n")
+       io.WriteString(f, emptyConfigYAML)
        newfile := f.Name()
 
        for _, trial := range []struct {
@@ -562,11 +564,122 @@ func (s *LoadSuite) TestListKeys(c *check.C) {
                c.Errorf("Should have produced an error")
        }
 
-       var logbuf bytes.Buffer
-       loader := testLoader(c, string(DefaultYAML), &logbuf)
+       loader := testLoader(c, string(DefaultYAML), nil)
        cfg, err := loader.Load()
        c.Assert(err, check.IsNil)
        if err := checkListKeys("", cfg); err != nil {
                c.Error(err)
        }
 }
+
+func (s *LoadSuite) TestImplicitStorageClasses(c *check.C) {
+       // If StorageClasses and Volumes.*.StorageClasses are all
+       // empty, there is a default storage class named "default".
+       ldr := testLoader(c, `{"Clusters":{"z1111":{}}}`, nil)
+       cfg, err := ldr.Load()
+       c.Assert(err, check.IsNil)
+       cc, err := cfg.GetCluster("z1111")
+       c.Assert(err, check.IsNil)
+       c.Check(cc.StorageClasses, check.HasLen, 1)
+       c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
+       c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
+
+       // The implicit "default" storage class is used by all
+       // volumes.
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  Volumes:
+   z: {}`, nil)
+       cfg, err = ldr.Load()
+       c.Assert(err, check.IsNil)
+       cc, err = cfg.GetCluster("z1111")
+       c.Assert(err, check.IsNil)
+       c.Check(cc.StorageClasses, check.HasLen, 1)
+       c.Check(cc.StorageClasses["default"].Default, check.Equals, true)
+       c.Check(cc.StorageClasses["default"].Priority, check.Equals, 0)
+       c.Check(cc.Volumes["z"].StorageClasses["default"], check.Equals, true)
+
+       // The "default" storage class isn't implicit if any classes
+       // are configured explicitly.
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  StorageClasses:
+   local:
+    Default: true
+    Priority: 111
+  Volumes:
+   z:
+    StorageClasses:
+     local: true`, nil)
+       cfg, err = ldr.Load()
+       c.Assert(err, check.IsNil)
+       cc, err = cfg.GetCluster("z1111")
+       c.Assert(err, check.IsNil)
+       c.Check(cc.StorageClasses, check.HasLen, 1)
+       c.Check(cc.StorageClasses["local"].Default, check.Equals, true)
+       c.Check(cc.StorageClasses["local"].Priority, check.Equals, 111)
+
+       // It is an error for a volume to refer to a storage class
+       // that isn't listed in StorageClasses.
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  StorageClasses:
+   local:
+    Default: true
+    Priority: 111
+  Volumes:
+   z:
+    StorageClasses:
+     nx: true`, nil)
+       _, err = ldr.Load()
+       c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "nx" that is not defined.*`)
+
+       // It is an error for a volume to refer to a storage class
+       // that isn't listed in StorageClasses ... even if it's
+       // "default", which would exist implicitly if it weren't
+       // referenced explicitly by a volume.
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  Volumes:
+   z:
+    StorageClasses:
+     default: true`, nil)
+       _, err = ldr.Load()
+       c.Assert(err, check.ErrorMatches, `z: volume refers to storage class "default" that is not defined.*`)
+
+       // If the "default" storage class is configured explicitly, it
+       // is not used implicitly by any volumes, even if it's the
+       // only storage class.
+       var logbuf bytes.Buffer
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  StorageClasses:
+   default:
+    Default: true
+    Priority: 111
+  Volumes:
+   z: {}`, &logbuf)
+       _, err = ldr.Load()
+       c.Assert(err, check.ErrorMatches, `z: volume has no StorageClasses listed`)
+
+       // If StorageClasses are configured explicitly, there must be
+       // at least one with Default: true. (Calling one "default" is
+       // not sufficient.)
+       ldr = testLoader(c, `
+Clusters:
+ z1111:
+  StorageClasses:
+   default:
+    Priority: 111
+  Volumes:
+   z:
+    StorageClasses:
+     default: true`, nil)
+       _, err = ldr.Load()
+       c.Assert(err, check.ErrorMatches, `there is no default storage class.*`)
+}
index 9e7eb521eec079a145c94a840a14b35e502b6f18..cc1de1be4295201f207da20eca3c436b84161ca0 100644 (file)
@@ -238,8 +238,9 @@ type Cluster struct {
                PreferDomainForUsername               string
                UserSetupMailText                     string
        }
-       Volumes   map[string]Volume
-       Workbench struct {
+       StorageClasses map[string]StorageClassConfig
+       Volumes        map[string]Volume
+       Workbench      struct {
                ActivationContactLink            string
                APIClientConnectTimeout          Duration
                APIClientReceiveTimeout          Duration
@@ -281,6 +282,11 @@ type Cluster struct {
        }
 }
 
+type StorageClassConfig struct {
+       Default  bool
+       Priority int
+}
+
 type Volume struct {
        AccessViaHosts   map[URL]VolumeAccess
        ReadOnly         bool
index ad04807712dd00d7e89e478be1a7eb2c01d0b057..d8e673bd343d772df9e76fcad6c68facefc55855 100644 (file)
@@ -913,7 +913,7 @@ class ArvPutUploadJob(object):
             self._local_collection = arvados.collection.Collection(
                 self._state['manifest'],
                 replication_desired=self.replication_desired,
-                storage_classes_desired=(self.storage_classes or ['default']),
+                storage_classes_desired=self.storage_classes,
                 put_threads=self.put_threads,
                 api_client=self._api_client,
                 num_retries=self.num_retries)
index 4e7b64cf5374fb38003d70790aebd8caee0931fb..d1d5ace0c3bbf78e413e30c96828ff4a914504bc 100644 (file)
@@ -17,7 +17,7 @@ class Collection < ArvadosModel
   # Posgresql JSONB columns should NOT be declared as serialized, Rails 5
   # already know how to properly treat them.
   attribute :properties, :jsonbHash, default: {}
-  attribute :storage_classes_desired, :jsonbArray, default: ["default"]
+  attribute :storage_classes_desired, :jsonbArray, default: lambda { Rails.configuration.DefaultStorageClasses }
   attribute :storage_classes_confirmed, :jsonbArray, default: []
 
   before_validation :default_empty_manifest
@@ -630,7 +630,7 @@ class Collection < ArvadosModel
   # validation on empty desired storage classes return an error.
   def default_storage_classes
     if self.storage_classes_desired.nil? || self.storage_classes_desired.empty?
-      self.storage_classes_desired = ["default"]
+      self.storage_classes_desired = Rails.configuration.DefaultStorageClasses
     end
     self.storage_classes_confirmed ||= []
   end
index af058494b2356628c73d9adb502a325d569e87ed..d6a44c80239d46c05d9d7ead49e14f538e19846d 100644 (file)
@@ -22,7 +22,7 @@ class Container < ArvadosModel
   attribute :secret_mounts, :jsonbHash, default: {}
   attribute :runtime_status, :jsonbHash, default: {}
   attribute :runtime_auth_scopes, :jsonbArray, default: []
-  attribute :output_storage_classes, :jsonbArray, default: ["default"]
+  attribute :output_storage_classes, :jsonbArray, default: lambda { Rails.configuration.DefaultStorageClasses }
 
   serialize :environment, Hash
   serialize :mounts, Hash
index 1de71102c61befff8e0aabef5885e7396405a237..4a580816cd737674966781d63dfee9a1b4a1dbee 100644 (file)
@@ -23,7 +23,7 @@ class ContainerRequest < ArvadosModel
   # already know how to properly treat them.
   attribute :properties, :jsonbHash, default: {}
   attribute :secret_mounts, :jsonbHash, default: {}
-  attribute :output_storage_classes, :jsonbArray, default: ["default"]
+  attribute :output_storage_classes, :jsonbArray, default: lambda { Rails.configuration.DefaultStorageClasses }
 
   serialize :environment, Hash
   serialize :mounts, Hash
index a6f1730e86190fc878c4c1ec3fd236af30e7cea3..1b3c96a8adfe55dd58085512969a59844d9a64d6 100644 (file)
@@ -170,6 +170,7 @@ arvcfg.declare_config "RemoteClusters", Hash, :remote_hosts, ->(cfg, k, v) {
   ConfigLoader.set_cfg cfg, "RemoteClusters", h
 }
 arvcfg.declare_config "RemoteClusters.*.Proxy", Boolean, :remote_hosts_via_dns
+arvcfg.declare_config "StorageClasses", Hash
 
 dbcfg = ConfigLoader.new
 
@@ -237,6 +238,17 @@ if $arvados_config["Collections"]["DefaultTrashLifetime"] < 86400.seconds then
   raise "default_trash_lifetime is %d, must be at least 86400" % Rails.configuration.Collections.DefaultTrashLifetime
 end
 
+default_storage_classes = []
+$arvados_config["StorageClasses"].each do |cls, cfg|
+  if cfg["Default"]
+    default_storage_classes << cls
+  end
+end
+if default_storage_classes.length == 0
+  default_storage_classes = ["default"]
+end
+$arvados_config["DefaultStorageClasses"] = default_storage_classes.sort
+
 #
 # Special case for test database where there's no database.yml,
 # because the Arvados config.yml doesn't have a concept of multiple
index 916ca095872db7a3a80d59799654dc32504e1f2b..e6912217d53fa5b484be8597ae6280465eda8ae5 100644 (file)
@@ -693,6 +693,19 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "storage_classes_desired default respects config" do
+    saved = Rails.configuration.DefaultStorageClasses
+    Rails.configuration.DefaultStorageClasses = ["foo"]
+    begin
+      act_as_user users(:active) do
+        c = Collection.create!
+        assert_equal ["foo"], c.storage_classes_desired
+      end
+    ensure
+      Rails.configuration.DefaultStorageClasses = saved
+    end
+  end
+
   test "storage_classes_desired cannot be empty" do
     act_as_user users(:active) do
       c = collections(:collection_owned_by_active)
index 7686e1a140618588fc15dcd8f71cb733d5af04c5..9f412c7bb09ad57a74dae753d83816c325cdefaa 100644 (file)
@@ -1292,14 +1292,20 @@ class ContainerRequestTest < ActiveSupport::TestCase
   end
 
   test "default output_storage_classes" do
-    act_as_user users(:active) do
-      cr = create_minimal_req!(priority: 1,
-                               state: ContainerRequest::Committed,
-                               output_name: 'foo')
-      run_container(cr)
-      cr.reload
-      output = Collection.find_by_uuid(cr.output_uuid)
-      assert_equal ["default"], output.storage_classes_desired
+    saved = Rails.configuration.DefaultStorageClasses
+    Rails.configuration.DefaultStorageClasses = ["foo"]
+    begin
+      act_as_user users(:active) do
+        cr = create_minimal_req!(priority: 1,
+                                 state: ContainerRequest::Committed,
+                                 output_name: 'foo')
+        run_container(cr)
+        cr.reload
+        output = Collection.find_by_uuid(cr.output_uuid)
+        assert_equal ["foo"], output.storage_classes_desired
+      end
+    ensure
+      Rails.configuration.DefaultStorageClasses = saved
     end
   end
 
index e69d941b1eaf6eadf52b5c48871fea02c0d3f5bb..bb590e13b33f0535d5a7d2610d8902ddab577300 100644 (file)
@@ -538,10 +538,6 @@ func (bal *Balancer) setupLookupTables() {
                        // effectively read-only.
                        mnt.ReadOnly = mnt.ReadOnly || srv.ReadOnly
 
-                       if len(mnt.StorageClasses) == 0 {
-                               bal.mountsByClass["default"][mnt] = true
-                               continue
-                       }
                        for class := range mnt.StorageClasses {
                                if mbc := bal.mountsByClass[class]; mbc == nil {
                                        bal.classes = append(bal.classes, class)
index 18a8bdcf47b111b0c6ea6469d58b60d2a10ea5f5..4e2c6803ca81cc593798d8285945aa9f90a1446c 100644 (file)
@@ -87,20 +87,24 @@ var stubServices = []arvados.KeepService{
 
 var stubMounts = map[string][]arvados.KeepMount{
        "keep0.zzzzz.arvadosapi.com:25107": {{
-               UUID:     "zzzzz-ivpuk-000000000000000",
-               DeviceID: "keep0-vol0",
+               UUID:           "zzzzz-ivpuk-000000000000000",
+               DeviceID:       "keep0-vol0",
+               StorageClasses: map[string]bool{"default": true},
        }},
        "keep1.zzzzz.arvadosapi.com:25107": {{
-               UUID:     "zzzzz-ivpuk-100000000000000",
-               DeviceID: "keep1-vol0",
+               UUID:           "zzzzz-ivpuk-100000000000000",
+               DeviceID:       "keep1-vol0",
+               StorageClasses: map[string]bool{"default": true},
        }},
        "keep2.zzzzz.arvadosapi.com:25107": {{
-               UUID:     "zzzzz-ivpuk-200000000000000",
-               DeviceID: "keep2-vol0",
+               UUID:           "zzzzz-ivpuk-200000000000000",
+               DeviceID:       "keep2-vol0",
+               StorageClasses: map[string]bool{"default": true},
        }},
        "keep3.zzzzz.arvadosapi.com:25107": {{
-               UUID:     "zzzzz-ivpuk-300000000000000",
-               DeviceID: "keep3-vol0",
+               UUID:           "zzzzz-ivpuk-300000000000000",
+               DeviceID:       "keep3-vol0",
+               StorageClasses: map[string]bool{"default": true},
        }},
 }
 
index 5bc66dbf3fa7b2f23828205b13452b5839d99510..c529ac150e092c37c9f5510d0463be6a0eebe553 100644 (file)
@@ -85,7 +85,8 @@ func (bal *balancerSuite) SetUpTest(c *check.C) {
                }
                srv.mounts = []*KeepMount{{
                        KeepMount: arvados.KeepMount{
-                               UUID: fmt.Sprintf("zzzzz-mount-%015x", i),
+                               UUID:           fmt.Sprintf("zzzzz-mount-%015x", i),
+                               StorageClasses: map[string]bool{"default": true},
                        },
                        KeepService: srv,
                }}
@@ -166,10 +167,11 @@ func (bal *balancerSuite) testMultipleViews(c *check.C, readonly bool) {
                srv.mounts[0].KeepMount.DeviceID = fmt.Sprintf("writable-by-srv-%x", i)
                srv.mounts = append(srv.mounts, &KeepMount{
                        KeepMount: arvados.KeepMount{
-                               DeviceID:    fmt.Sprintf("writable-by-srv-%x", (i+1)%len(bal.srvs)),
-                               UUID:        fmt.Sprintf("zzzzz-mount-%015x", i<<16),
-                               ReadOnly:    readonly,
-                               Replication: 1,
+                               DeviceID:       fmt.Sprintf("writable-by-srv-%x", (i+1)%len(bal.srvs)),
+                               UUID:           fmt.Sprintf("zzzzz-mount-%015x", i<<16),
+                               ReadOnly:       readonly,
+                               Replication:    1,
+                               StorageClasses: map[string]bool{"default": true},
                        },
                        KeepService: srv,
                })
index db64449e48bed2234ae32a97fbc122b360a77576..897447dd11c7a95a5b113d867fb0de28cbed6844 100644 (file)
@@ -21,7 +21,6 @@ import (
        "net/http"
        "net/http/httptest"
        "os"
-       "regexp"
        "sort"
        "strings"
        "time"
@@ -320,6 +319,54 @@ func (s *HandlerSuite) TestPutAndDeleteSkipReadonlyVolumes(c *check.C) {
        }
 }
 
+func (s *HandlerSuite) TestReadsOrderedByStorageClassPriority(c *check.C) {
+       s.cluster.Volumes = map[string]arvados.Volume{
+               "zzzzz-nyw5e-111111111111111": {
+                       Driver:         "mock",
+                       Replication:    1,
+                       StorageClasses: map[string]bool{"class1": true}},
+               "zzzzz-nyw5e-222222222222222": {
+                       Driver:         "mock",
+                       Replication:    1,
+                       StorageClasses: map[string]bool{"class2": true, "class3": true}},
+       }
+
+       for _, trial := range []struct {
+               priority1 int // priority of class1, thus vol1
+               priority2 int // priority of class2
+               priority3 int // priority of class3 (vol2 priority will be max(priority2, priority3))
+               get1      int // expected number of "get" ops on vol1
+               get2      int // expected number of "get" ops on vol2
+       }{
+               {100, 50, 50, 1, 0},   // class1 has higher priority => try vol1 first, no need to try vol2
+               {100, 100, 100, 1, 0}, // same priority, vol1 is first lexicographically => try vol1 first and succeed
+               {66, 99, 33, 1, 1},    // class2 has higher priority => try vol2 first, then try vol1
+               {66, 33, 99, 1, 1},    // class3 has highest priority => vol2 has highest => try vol2 first, then try vol1
+       } {
+               c.Logf("%+v", trial)
+               s.cluster.StorageClasses = map[string]arvados.StorageClassConfig{
+                       "class1": {Priority: trial.priority1},
+                       "class2": {Priority: trial.priority2},
+                       "class3": {Priority: trial.priority3},
+               }
+               c.Assert(s.handler.setup(context.Background(), s.cluster, "", prometheus.NewRegistry(), testServiceURL), check.IsNil)
+               IssueRequest(s.handler,
+                       &RequestTester{
+                               method:         "PUT",
+                               uri:            "/" + TestHash,
+                               requestBody:    TestBlock,
+                               storageClasses: "class1",
+                       })
+               IssueRequest(s.handler,
+                       &RequestTester{
+                               method: "GET",
+                               uri:    "/" + TestHash,
+                       })
+               c.Check(s.handler.volmgr.mountMap["zzzzz-nyw5e-111111111111111"].Volume.(*MockVolume).CallCount("Get"), check.Equals, trial.get1)
+               c.Check(s.handler.volmgr.mountMap["zzzzz-nyw5e-222222222222222"].Volume.(*MockVolume).CallCount("Get"), check.Equals, trial.get2)
+       }
+}
+
 // Test TOUCH requests.
 func (s *HandlerSuite) TestTouchHandler(c *check.C) {
        c.Assert(s.handler.setup(context.Background(), s.cluster, "", prometheus.NewRegistry(), testServiceURL), check.IsNil)
@@ -497,12 +544,8 @@ func (s *HandlerSuite) TestIndexHandler(c *check.C) {
 
        expected := `^` + TestHash + `\+\d+ \d+\n` +
                TestHash2 + `\+\d+ \d+\n\n$`
-       match, _ := regexp.MatchString(expected, response.Body.String())
-       if !match {
-               c.Errorf(
-                       "permissions on, superuser request: expected %s, got:\n%s",
-                       expected, response.Body.String())
-       }
+       c.Check(response.Body.String(), check.Matches, expected, check.Commentf(
+               "permissions on, superuser request"))
 
        // superuser /index/prefix request
        // => OK
@@ -513,12 +556,8 @@ func (s *HandlerSuite) TestIndexHandler(c *check.C) {
                response)
 
        expected = `^` + TestHash + `\+\d+ \d+\n\n$`
-       match, _ = regexp.MatchString(expected, response.Body.String())
-       if !match {
-               c.Errorf(
-                       "permissions on, superuser /index/prefix request: expected %s, got:\n%s",
-                       expected, response.Body.String())
-       }
+       c.Check(response.Body.String(), check.Matches, expected, check.Commentf(
+               "permissions on, superuser /index/prefix request"))
 
        // superuser /index/{no-such-prefix} request
        // => OK
index a60d17d576ae047b8a3198cd8c755c3828ef9875..2b469a13eb993e0827bac8ae1ebe4db46bc8c4df 100644 (file)
@@ -252,6 +252,13 @@ func (rtr *router) handlePUT(resp http.ResponseWriter, req *http.Request) {
                for i, sc := range wantStorageClasses {
                        wantStorageClasses[i] = strings.TrimSpace(sc)
                }
+       } else {
+               // none specified -- use configured default
+               for class, cfg := range rtr.cluster.StorageClasses {
+                       if cfg.Default {
+                               wantStorageClasses = append(wantStorageClasses, class)
+                       }
+               }
        }
 
        buf, err := getBufferWithContext(ctx, bufs, int(req.ContentLength))
index 26e6b731828f9be0861044cb6a7c4e10d097d05f..9bfc6ca3e5191d2953ceac75f915a07cab19c69f 100644 (file)
@@ -10,6 +10,7 @@ import (
        "fmt"
        "io"
        "math/big"
+       "sort"
        "sync/atomic"
        "time"
 
@@ -343,6 +344,27 @@ func makeRRVolumeManager(logger logrus.FieldLogger, cluster *arvados.Cluster, my
                        vm.writables = append(vm.writables, mnt)
                }
        }
+       // pri(i): return highest priority of any storage class
+       // offered by vm.readables[i]
+       pri := func(i int) int {
+               any, best := false, 0
+               for class := range vm.readables[i].KeepMount.StorageClasses {
+                       if p := cluster.StorageClasses[class].Priority; !any || best < p {
+                               best = p
+                               any = true
+                       }
+               }
+               return best
+       }
+       // sort vm.readables, first by highest priority of any offered
+       // storage class (highest->lowest), then by volume UUID
+       sort.Slice(vm.readables, func(i, j int) bool {
+               if pi, pj := pri(i), pri(j); pi != pj {
+                       return pi > pj
+               } else {
+                       return vm.readables[i].KeepMount.UUID < vm.readables[j].KeepMount.UUID
+               }
+       })
        return vm, nil
 }