diff --git a/cmd/admin-server-info.go b/cmd/admin-server-info.go index 2a5b29252..0105ed26e 100644 --- a/cmd/admin-server-info.go +++ b/cmd/admin-server-info.go @@ -31,7 +31,10 @@ import ( // local endpoints from given list of endpoints func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Request) madmin.ServerProperties { var localEndpoints Endpoints - addr := r.Host + addr := globalLocalNodeName + if r != nil { + addr = r.Host + } if globalIsDistErasure { addr = globalLocalNodeName } @@ -40,7 +43,7 @@ func getLocalServerProperty(endpointServerPools EndpointServerPools, r *http.Req for _, endpoint := range ep.Endpoints { nodeName := endpoint.Host if nodeName == "" { - nodeName = r.Host + nodeName = addr } if endpoint.IsLocal { // Only proceed for local endpoints diff --git a/cmd/callhome.go b/cmd/callhome.go new file mode 100644 index 000000000..08a13f8ff --- /dev/null +++ b/cmd/callhome.go @@ -0,0 +1,136 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/minio/madmin-go" + "github.com/minio/minio/internal/logger" + uatomic "go.uber.org/atomic" +) + +const ( + // callhomeSchemaVersion1 is callhome schema version 1 + callhomeSchemaVersion1 = "1" + + // callhomeSchemaVersion is current callhome schema version. + callhomeSchemaVersion = callhomeSchemaVersion1 + + // callhomeCycleDefault is the default interval between two callhome cycles (24hrs) + callhomeCycleDefault = 24 * time.Hour +) + +// CallhomeInfo - Contains callhome information +type CallhomeInfo struct { + SchemaVersion string `json:"schema_version"` + AdminInfo madmin.InfoMessage `json:"admin_info"` +} + +var ( + enableCallhome = uatomic.NewBool(false) + callhomeLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second) + callhomeFreq = uatomic.NewDuration(callhomeCycleDefault) +) + +func updateCallhomeParams(ctx context.Context, objAPI ObjectLayer) { + alreadyEnabled := enableCallhome.Load() + enableCallhome.Store(globalCallhomeConfig.Enable) + callhomeFreq.Store(globalCallhomeConfig.Frequency) + + // If callhome was disabled earlier and has now been enabled, + // initialize the callhome process again. + if !alreadyEnabled && enableCallhome.Load() { + initCallhome(ctx, objAPI) + } +} + +// initCallhome will start the callhome task in the background. +func initCallhome(ctx context.Context, objAPI ObjectLayer) { + go func() { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + // Leader node (that successfully acquires the lock inside runCallhome) + // will keep performing the callhome. If the leader goes down for some reason, + // the lock will be released and another node will acquire it and take over + // because of this loop. + for { + runCallhome(ctx, objAPI) + if !enableCallhome.Load() { + return + } + + // callhome running on a different node. + // sleep for some time and try again. + duration := time.Duration(r.Float64() * float64(callhomeFreq.Load())) + if duration < time.Second { + // Make sure to sleep atleast a second to avoid high CPU ticks. + duration = time.Second + } + time.Sleep(duration) + + if !enableCallhome.Load() { + return + } + } + }() +} + +func runCallhome(ctx context.Context, objAPI ObjectLayer) { + // Make sure only 1 callhome is running on the cluster. + locker := objAPI.NewNSLock(minioMetaBucket, "callhome/runCallhome.lock") + lkctx, err := locker.GetLock(ctx, callhomeLeaderLockTimeout) + if err != nil { + return + } + + ctx = lkctx.Context() + defer locker.Unlock(lkctx.Cancel) + + callhomeTimer := time.NewTimer(callhomeFreq.Load()) + defer callhomeTimer.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-callhomeTimer.C: + if !enableCallhome.Load() { + // Stop the processing as callhome got disabled + return + } + performCallhome(ctx) + + // Reset the timer for next cycle. + callhomeTimer.Reset(callhomeFreq.Load()) + } + } +} + +func performCallhome(ctx context.Context) { + err := sendCallhomeInfo( + CallhomeInfo{ + SchemaVersion: callhomeSchemaVersion, + AdminInfo: getServerInfo(ctx, nil), + }) + if err != nil { + logger.LogIf(ctx, fmt.Errorf("Unable to perform callhome: %w", err)) + } +} diff --git a/cmd/common-main.go b/cmd/common-main.go index 5bdbab2d9..5dbd12f54 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -230,8 +230,8 @@ func minioConfigToConsoleFeatures() { if globalSubnetConfig.APIKey != "" { os.Setenv("CONSOLE_SUBNET_API_KEY", globalSubnetConfig.APIKey) } - if globalSubnetConfig.Proxy != "" { - os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.Proxy) + if globalSubnetConfig.ProxyURL != nil { + os.Setenv("CONSOLE_SUBNET_PROXY", globalSubnetConfig.ProxyURL.String()) } } diff --git a/cmd/config-current.go b/cmd/config-current.go index 74e94e4c2..b667b1a8a 100644 --- a/cmd/config-current.go +++ b/cmd/config-current.go @@ -28,6 +28,7 @@ import ( "github.com/minio/minio/internal/config" "github.com/minio/minio/internal/config/api" "github.com/minio/minio/internal/config/cache" + "github.com/minio/minio/internal/config/callhome" "github.com/minio/minio/internal/config/compress" "github.com/minio/minio/internal/config/dns" "github.com/minio/minio/internal/config/etcd" @@ -70,6 +71,7 @@ func initHelp() { config.HealSubSys: heal.DefaultKVS, config.ScannerSubSys: scanner.DefaultKVS, config.SubnetSubSys: subnet.DefaultKVS, + config.CallhomeSubSys: callhome.DefaultKVS, } for k, v := range notify.DefaultNotificationKVS { kvs[k] = v @@ -201,6 +203,12 @@ func initHelp() { Description: "set subnet config for the cluster e.g. api key", Optional: true, }, + config.HelpKV{ + Key: config.CallhomeSubSys, + Type: "string", + Description: "enable callhome for the cluster", + Optional: true, + }, } if globalIsErasure { @@ -243,6 +251,7 @@ func initHelp() { config.NotifyWebhookSubSys: notify.HelpWebhook, config.NotifyESSubSys: notify.HelpES, config.SubnetSubSys: subnet.HelpSubnet, + config.CallhomeSubSys: callhome.HelpCallhome, } config.RegisterHelpSubSys(helpMap) @@ -358,6 +367,10 @@ func validateSubSysConfig(s config.Config, subSys string, objAPI ObjectLayer) er if _, err := subnet.LookupConfig(s[config.SubnetSubSys][config.Default]); err != nil { return err } + case config.CallhomeSubSys: + if _, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default]); err != nil { + return err + } case config.PolicyOPASubSys: // In case legacy OPA config is being set, we treat it as if the // AuthZPlugin is being set. @@ -649,7 +662,7 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf return fmt.Errorf("Unable to apply scanner config: %w", err) } // update dynamic scanner values. - scannerCycle.Update(scannerCfg.Cycle) + scannerCycle.Store(scannerCfg.Cycle) logger.LogIf(ctx, scannerSleeper.Update(scannerCfg.Delay, scannerCfg.MaxWait)) case config.LoggerWebhookSubSys: loggerCfg, err := logger.LookupConfigForSubSys(s, config.LoggerWebhookSubSys) @@ -719,6 +732,14 @@ func applyDynamicConfigForSubSys(ctx context.Context, objAPI ObjectLayer, s conf } } } + case config.CallhomeSubSys: + callhomeCfg, err := callhome.LookupConfig(s[config.CallhomeSubSys][config.Default]) + if err != nil { + logger.LogIf(ctx, fmt.Errorf("Unable to load callhome config: %w", err)) + } else { + globalCallhomeConfig = callhomeCfg + updateCallhomeParams(ctx, objAPI) + } } globalServerConfigMu.Lock() defer globalServerConfigMu.Unlock() diff --git a/cmd/data-scanner.go b/cmd/data-scanner.go index bb52bd1a5..343853395 100644 --- a/cmd/data-scanner.go +++ b/cmd/data-scanner.go @@ -45,6 +45,7 @@ import ( "github.com/minio/minio/internal/event" "github.com/minio/minio/internal/logger" "github.com/minio/pkg/console" + uatomic "go.uber.org/atomic" ) const ( @@ -66,9 +67,7 @@ var ( dataScannerLeaderLockTimeout = newDynamicTimeout(30*time.Second, 10*time.Second) // Sleeper values are updated when config is loaded. scannerSleeper = newDynamicSleeper(10, 10*time.Second) - scannerCycle = &safeDuration{ - t: dataScannerStartDelay, - } + scannerCycle = uatomic.NewDuration(dataScannerStartDelay) ) // initDataScanner will start the scanner in the background. @@ -78,7 +77,7 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) { // Run the data scanner in a loop for { runDataScanner(ctx, objAPI) - duration := time.Duration(r.Float64() * float64(scannerCycle.Get())) + duration := time.Duration(r.Float64() * float64(scannerCycle.Load())) if duration < time.Second { // Make sure to sleep atleast a second to avoid high CPU ticks. duration = time.Second @@ -88,23 +87,6 @@ func initDataScanner(ctx context.Context, objAPI ObjectLayer) { }() } -type safeDuration struct { - sync.Mutex - t time.Duration -} - -func (s *safeDuration) Update(t time.Duration) { - s.Lock() - defer s.Unlock() - s.t = t -} - -func (s *safeDuration) Get() time.Duration { - s.Lock() - defer s.Unlock() - return s.t -} - func getCycleScanMode(currentCycle, bitrotStartCycle uint64, bitrotStartTime time.Time) madmin.HealScanMode { bitrotCycle := globalHealConfig.BitrotScanCycle() switch bitrotCycle { @@ -189,7 +171,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) { } } - scannerTimer := time.NewTimer(scannerCycle.Get()) + scannerTimer := time.NewTimer(scannerCycle.Load()) defer scannerTimer.Stop() for { @@ -231,7 +213,7 @@ func runDataScanner(pctx context.Context, objAPI ObjectLayer) { } // Reset the timer for next cycle. - scannerTimer.Reset(scannerCycle.Get()) + scannerTimer.Reset(scannerCycle.Load()) } } } diff --git a/cmd/globals.go b/cmd/globals.go index 9fa15020d..8063aa141 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -37,6 +37,7 @@ import ( "github.com/dustin/go-humanize" "github.com/minio/minio/internal/auth" "github.com/minio/minio/internal/config/cache" + "github.com/minio/minio/internal/config/callhome" "github.com/minio/minio/internal/config/compress" "github.com/minio/minio/internal/config/dns" xldap "github.com/minio/minio/internal/config/identity/ldap" @@ -243,6 +244,9 @@ var ( // The global subnet config globalSubnetConfig subnet.Config + // The global callhome config + globalCallhomeConfig callhome.Config + globalRemoteEndpoints map[string]Endpoint // Global server's network statistics diff --git a/cmd/subnet-utils.go b/cmd/subnet-utils.go new file mode 100644 index 000000000..cdcf90342 --- /dev/null +++ b/cmd/subnet-utils.go @@ -0,0 +1,102 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "time" + + xhttp "github.com/minio/minio/internal/http" +) + +const ( + subnetRespBodyLimit = 1 << 20 // 1 MiB + callhomeURL = "https://subnet.min.io/api/callhome" + callhomeURLDev = "http://localhost:9000/api/callhome" +) + +func httpClient(timeout time.Duration) *http.Client { + return &http.Client{ + Timeout: timeout, + Transport: globalProxyTransport, + } +} + +func subnetHTTPDo(req *http.Request) (*http.Response, error) { + client := httpClient(10 * time.Second) + if globalSubnetConfig.ProxyURL != nil { + client.Transport.(*http.Transport).Proxy = http.ProxyURL((*url.URL)(globalSubnetConfig.ProxyURL)) + } + return client.Do(req) +} + +func subnetReqDo(r *http.Request, authToken string) (string, error) { + r.Header.Set("Authorization", authToken) + r.Header.Set("Content-Type", "application/json") + + resp, err := subnetHTTPDo(r) + if resp != nil { + defer xhttp.DrainBody(resp.Body) + } + if err != nil { + return "", err + } + + respBytes, err := ioutil.ReadAll(io.LimitReader(resp.Body, subnetRespBodyLimit)) + if err != nil { + return "", err + } + respStr := string(respBytes) + + if resp.StatusCode == http.StatusOK { + return respStr, nil + } + return respStr, fmt.Errorf("SUBNET request failed with code %d and error: %s", resp.StatusCode, respStr) +} + +func subnetPostReq(reqURL string, payload interface{}, authToken string) (string, error) { + body, err := json.Marshal(payload) + if err != nil { + return "", err + } + r, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewReader(body)) + if err != nil { + return "", err + } + return subnetReqDo(r, authToken) +} + +func sendCallhomeInfo(ch CallhomeInfo) error { + if len(globalSubnetConfig.APIKey) == 0 { + return errors.New("Cluster is not registered with SUBNET. Please register by running 'mc support register ALIAS'") + } + + url := callhomeURL + if globalIsCICD { + url = callhomeURLDev + } + _, err := subnetPostReq(url, ch, globalSubnetConfig.APIKey) + return err +} diff --git a/internal/config/callhome/callhome.go b/internal/config/callhome/callhome.go new file mode 100644 index 000000000..4bfa13204 --- /dev/null +++ b/internal/config/callhome/callhome.go @@ -0,0 +1,65 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package callhome + +import ( + "time" + + "github.com/minio/minio/internal/config" + "github.com/minio/pkg/env" +) + +// Callhome related keys +const ( + Enable = "enable" + Frequency = "frequency" +) + +// DefaultKVS - default KV config for subnet settings +var DefaultKVS = config.KVS{ + config.KV{ + Key: Enable, + Value: "off", + }, + config.KV{ + Key: Frequency, + Value: "24h", + }, +} + +// Config represents the subnet related configuration +type Config struct { + // Flag indicating whether callhome is enabled. + Enable bool `json:"enable"` + + // The interval between callhome cycles + Frequency time.Duration `json:"frequency"` +} + +// LookupConfig - lookup config and override with valid environment settings if any. +func LookupConfig(kvs config.KVS) (cfg Config, err error) { + if err = config.CheckValidKeys(config.CallhomeSubSys, kvs, DefaultKVS); err != nil { + return cfg, err + } + + cfg.Enable = env.Get(config.EnvMinIOCallhomeEnable, + kvs.GetWithDefault(Enable, DefaultKVS)) == config.EnableOn + cfg.Frequency, err = time.ParseDuration(env.Get(config.EnvMinIOCallhomeFrequency, + kvs.GetWithDefault(Frequency, DefaultKVS))) + return cfg, err +} diff --git a/internal/config/callhome/help.go b/internal/config/callhome/help.go new file mode 100644 index 000000000..8def3fa5b --- /dev/null +++ b/internal/config/callhome/help.go @@ -0,0 +1,42 @@ +// Copyright (c) 2015-2022 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package callhome + +import "github.com/minio/minio/internal/config" + +var ( + defaultHelpPostfix = func(key string) string { + return config.DefaultHelpPostfix(DefaultKVS, key) + } + + // HelpCallhome - provides help for callhome config + HelpCallhome = config.HelpKVS{ + config.HelpKV{ + Key: Enable, + Type: "on|off", + Description: "set to enable callhome" + defaultHelpPostfix(Enable), + Optional: true, + }, + config.HelpKV{ + Key: Frequency, + Type: "duration", + Description: "time duration between callhome cycles e.g. 24h" + defaultHelpPostfix(Frequency), + Optional: true, + }, + } +) diff --git a/internal/config/config.go b/internal/config/config.go index 114534e77..3ca9dd03d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -88,6 +88,7 @@ const ( ScannerSubSys = "scanner" CrawlerSubSys = "crawler" SubnetSubSys = "subnet" + CallhomeSubSys = "callhome" // Add new constants here if you add new fields to config. ) @@ -161,6 +162,7 @@ var SubSystems = set.CreateStringSet( NotifyRedisSubSys, NotifyWebhookSubSys, SubnetSubSys, + CallhomeSubSys, ) // SubSystemsDynamic - all sub-systems that have dynamic config. @@ -170,6 +172,7 @@ var SubSystemsDynamic = set.CreateStringSet( ScannerSubSys, HealSubSys, SubnetSubSys, + CallhomeSubSys, LoggerWebhookSubSys, AuditWebhookSubSys, AuditKafkaSubSys, diff --git a/internal/config/constants.go b/internal/config/constants.go index 05a9f2674..f55655209 100644 --- a/internal/config/constants.go +++ b/internal/config/constants.go @@ -53,9 +53,13 @@ const ( EnvSiteName = "MINIO_SITE_NAME" EnvSiteRegion = "MINIO_SITE_REGION" - EnvMinIOSubnetLicense = "MINIO_SUBNET_LICENSE" // Deprecated Dec 2021 - EnvMinIOSubnetAPIKey = "MINIO_SUBNET_API_KEY" - EnvMinIOSubnetProxy = "MINIO_SUBNET_PROXY" + EnvMinIOSubnetLicense = "MINIO_SUBNET_LICENSE" // Deprecated Dec 2021 + EnvMinIOSubnetAPIKey = "MINIO_SUBNET_API_KEY" + EnvMinIOSubnetProxy = "MINIO_SUBNET_PROXY" + + EnvMinIOCallhomeEnable = "MINIO_CALLHOME_ENABLE" + EnvMinIOCallhomeFrequency = "MINIO_CALLHOME_FREQUENCY" + EnvMinIOServerURL = "MINIO_SERVER_URL" EnvMinIOBrowserRedirectURL = "MINIO_BROWSER_REDIRECT_URL" EnvRootDiskThresholdSize = "MINIO_ROOTDISK_THRESHOLD_SIZE" diff --git a/internal/config/subnet/api-key.go b/internal/config/subnet/api-key.go index 03a1aedab..f1bb1859c 100644 --- a/internal/config/subnet/api-key.go +++ b/internal/config/subnet/api-key.go @@ -18,10 +18,9 @@ package subnet import ( - "net/url" - "github.com/minio/minio/internal/config" "github.com/minio/pkg/env" + xnet "github.com/minio/pkg/net" ) // DefaultKVS - default KV config for subnet settings @@ -49,7 +48,7 @@ type Config struct { APIKey string `json:"api_key"` // The HTTP(S) proxy URL to use for connecting to SUBNET - Proxy string `json:"proxy"` + ProxyURL *xnet.URL `json:"proxy_url"` } // LookupConfig - lookup config and override with valid environment settings if any. @@ -58,10 +57,12 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) { return cfg, err } - cfg.Proxy = env.Get(config.EnvMinIOSubnetProxy, kvs.Get(config.Proxy)) - _, err = url.Parse(cfg.Proxy) - if err != nil { - return cfg, err + proxy := env.Get(config.EnvMinIOSubnetProxy, kvs.Get(config.Proxy)) + if len(proxy) > 0 { + cfg.ProxyURL, err = xnet.ParseHTTPURL(proxy) + if err != nil { + return cfg, err + } } cfg.License = env.Get(config.EnvMinIOSubnetLicense, kvs.Get(config.License))