Merge branch '15370-loopback-dispatchcloud'
[arvados.git] / lib / config / load.go
index 8f8ab2bf27312adb76b6597ef1f51f8e21ecb338..fbd01488a0be51c430c0c6efc9ef7862ebb88fe5 100644 (file)
@@ -16,6 +16,7 @@ import (
        "io/ioutil"
        "os"
        "regexp"
+       "runtime"
        "strconv"
        "strings"
        "time"
@@ -25,6 +26,7 @@ import (
        "github.com/imdario/mergo"
        "github.com/prometheus/client_golang/prometheus"
        "github.com/sirupsen/logrus"
+       "golang.org/x/sys/unix"
 )
 
 //go:embed config.default.yml
@@ -297,7 +299,10 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                        ldr.loadOldKeepBalanceConfig,
                )
        }
-       loadFuncs = append(loadFuncs, ldr.setImplicitStorageClasses)
+       loadFuncs = append(loadFuncs,
+               ldr.setImplicitStorageClasses,
+               ldr.setLoopbackInstanceType,
+       )
        for _, f := range loadFuncs {
                err = f(&cfg)
                if err != nil {
@@ -340,6 +345,7 @@ func (ldr *Loader) Load() (*arvados.Config, error) {
                        ldr.checkEnum("Containers.LocalKeepLogsToContainerLog", cc.Containers.LocalKeepLogsToContainerLog, "none", "all", "errors"),
                        ldr.checkEmptyKeepstores(cc),
                        ldr.checkUnlistedKeepstores(cc),
+                       ldr.checkLocalKeepBlobBuffers(cc),
                        ldr.checkStorageClasses(cc),
                        ldr.checkCUDAVersions(cc),
                        // TODO: check non-empty Rendezvous on
@@ -361,7 +367,7 @@ func (ldr *Loader) checkClusterID(label, clusterID string, emptyStringOk bool) e
        if emptyStringOk && clusterID == "" {
                return nil
        } else if !acceptableClusterIDRe.MatchString(clusterID) {
-               return fmt.Errorf("%s: cluster ID should be 5 alphanumeric characters", label)
+               return fmt.Errorf("%s: cluster ID should be 5 lowercase alphanumeric characters", label)
        }
        return nil
 }
@@ -414,6 +420,67 @@ func (ldr *Loader) checkEnum(label, value string, accepted ...string) error {
        return fmt.Errorf("%s: unacceptable value %q: must be one of %q", label, value, accepted)
 }
 
+func (ldr *Loader) setLoopbackInstanceType(cfg *arvados.Config) error {
+       for id, cc := range cfg.Clusters {
+               if !cc.Containers.CloudVMs.Enable || cc.Containers.CloudVMs.Driver != "loopback" {
+                       continue
+               }
+               if len(cc.InstanceTypes) == 1 {
+                       continue
+               }
+               if len(cc.InstanceTypes) > 1 {
+                       return fmt.Errorf("Clusters.%s.InstanceTypes: cannot use multiple InstanceTypes with loopback driver", id)
+               }
+               // No InstanceTypes configured. Fill in implicit
+               // default.
+               hostram, err := getHostRAM()
+               if err != nil {
+                       return err
+               }
+               scratch, err := getFilesystemSize(os.TempDir())
+               if err != nil {
+                       return err
+               }
+               cc.InstanceTypes = arvados.InstanceTypeMap{"localhost": {
+                       Name:            "localhost",
+                       ProviderType:    "localhost",
+                       VCPUs:           runtime.NumCPU(),
+                       RAM:             hostram,
+                       Scratch:         scratch,
+                       IncludedScratch: scratch,
+               }}
+               cfg.Clusters[id] = cc
+       }
+       return nil
+}
+
+func getFilesystemSize(path string) (arvados.ByteSize, error) {
+       var stat unix.Statfs_t
+       err := unix.Statfs(path, &stat)
+       if err != nil {
+               return 0, err
+       }
+       return arvados.ByteSize(stat.Blocks * uint64(stat.Bsize)), nil
+}
+
+var reMemTotal = regexp.MustCompile(`(^|\n)MemTotal: *(\d+) kB\n`)
+
+func getHostRAM() (arvados.ByteSize, error) {
+       buf, err := os.ReadFile("/proc/meminfo")
+       if err != nil {
+               return 0, err
+       }
+       m := reMemTotal.FindSubmatch(buf)
+       if m == nil {
+               return 0, errors.New("error parsing /proc/meminfo: no MemTotal")
+       }
+       kb, err := strconv.ParseInt(string(m[2]), 10, 64)
+       if err != nil {
+               return 0, fmt.Errorf("error parsing /proc/meminfo: %q: %w", m[2], err)
+       }
+       return arvados.ByteSize(kb) * 1024, nil
+}
+
 func (ldr *Loader) setImplicitStorageClasses(cfg *arvados.Config) error {
 cluster:
        for id, cc := range cfg.Clusters {
@@ -437,6 +504,24 @@ cluster:
        return nil
 }
 
+func (ldr *Loader) checkLocalKeepBlobBuffers(cc arvados.Cluster) error {
+       kbb := cc.Containers.LocalKeepBlobBuffersPerVCPU
+       if kbb == 0 {
+               return nil
+       }
+       for uuid, vol := range cc.Volumes {
+               if len(vol.AccessViaHosts) > 0 {
+                       ldr.Logger.Warnf("LocalKeepBlobBuffersPerVCPU is %d but will not be used because at least one volume (%s) uses AccessViaHosts -- suggest changing to 0", kbb, uuid)
+                       return nil
+               }
+               if !vol.ReadOnly && vol.Replication < cc.Collections.DefaultReplication {
+                       ldr.Logger.Warnf("LocalKeepBlobBuffersPerVCPU is %d but will not be used because at least one volume (%s) has lower replication than DefaultReplication (%d < %d) -- suggest changing to 0", kbb, uuid, vol.Replication, cc.Collections.DefaultReplication)
+                       return nil
+               }
+       }
+       return nil
+}
+
 func (ldr *Loader) checkStorageClasses(cc arvados.Cluster) error {
        classOnVolume := map[string]bool{}
        for volid, vol := range cc.Volumes {