You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

595 lines
16 KiB

  1. // Copyright (c) 2015-2024 MinIO, Inc
  2. //
  3. // This file is part of MinIO Object Storage stack
  4. //
  5. // This program is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU Affero General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // This program is distributed in the hope that it will be useful
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU Affero General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU Affero General Public License
  16. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. package cmd
  18. import (
  19. "bytes"
  20. "context"
  21. "encoding/base64"
  22. "encoding/binary"
  23. "errors"
  24. "fmt"
  25. "math/rand"
  26. "net/http"
  27. "path"
  28. "strings"
  29. "sync"
  30. "time"
  31. "github.com/minio/madmin-go/v3"
  32. "github.com/minio/minio/internal/crypto"
  33. "github.com/minio/minio/internal/hash"
  34. "github.com/minio/minio/internal/kms"
  35. "github.com/prometheus/client_golang/prometheus"
  36. )
  37. //go:generate msgp -file $GOFILE
  38. var (
  39. errTierMissingCredentials = AdminError{
  40. Code: "XMinioAdminTierMissingCredentials",
  41. Message: "Specified remote credentials are empty",
  42. StatusCode: http.StatusForbidden,
  43. }
  44. errTierBackendInUse = AdminError{
  45. Code: "XMinioAdminTierBackendInUse",
  46. Message: "Specified remote tier is already in use",
  47. StatusCode: http.StatusConflict,
  48. }
  49. errTierTypeUnsupported = AdminError{
  50. Code: "XMinioAdminTierTypeUnsupported",
  51. Message: "Specified tier type is unsupported",
  52. StatusCode: http.StatusBadRequest,
  53. }
  54. errTierBackendNotEmpty = AdminError{
  55. Code: "XMinioAdminTierBackendNotEmpty",
  56. Message: "Specified remote backend is not empty",
  57. StatusCode: http.StatusBadRequest,
  58. }
  59. errTierInvalidConfig = AdminError{
  60. Code: "XMinioAdminTierInvalidConfig",
  61. Message: "Unable to setup remote tier, check tier configuration",
  62. StatusCode: http.StatusBadRequest,
  63. }
  64. )
  65. const (
  66. tierConfigFile = "tier-config.bin"
  67. tierConfigFormat = 1
  68. tierConfigV1 = 1
  69. tierConfigVersion = 2
  70. )
  71. // tierConfigPath refers to remote tier config object name
  72. var tierConfigPath = path.Join(minioConfigPrefix, tierConfigFile)
  73. const tierCfgRefreshAtHdr = "X-MinIO-TierCfg-RefreshedAt"
  74. // TierConfigMgr holds the collection of remote tiers configured in this deployment.
  75. type TierConfigMgr struct {
  76. sync.RWMutex `msg:"-"`
  77. drivercache map[string]WarmBackend `msg:"-"`
  78. Tiers map[string]madmin.TierConfig `json:"tiers"`
  79. lastRefreshedAt time.Time `msg:"-"`
  80. }
  81. type tierMetrics struct {
  82. sync.RWMutex // protects requestsCount only
  83. requestsCount map[string]struct {
  84. success int64
  85. failure int64
  86. }
  87. histogram *prometheus.HistogramVec
  88. }
  89. var globalTierMetrics = tierMetrics{
  90. requestsCount: make(map[string]struct {
  91. success int64
  92. failure int64
  93. }),
  94. histogram: prometheus.NewHistogramVec(prometheus.HistogramOpts{
  95. Name: "tier_ttlb_seconds",
  96. Help: "Time taken by requests served by warm tier",
  97. Buckets: []float64{0.01, 0.1, 1, 2, 5, 10, 60, 5 * 60, 15 * 60, 30 * 60},
  98. }, []string{"tier"}),
  99. }
  100. func (t *tierMetrics) Observe(tier string, dur time.Duration) {
  101. t.histogram.With(prometheus.Labels{"tier": tier}).Observe(dur.Seconds())
  102. }
  103. func (t *tierMetrics) logSuccess(tier string) {
  104. t.Lock()
  105. defer t.Unlock()
  106. stat := t.requestsCount[tier]
  107. stat.success++
  108. t.requestsCount[tier] = stat
  109. }
  110. func (t *tierMetrics) logFailure(tier string) {
  111. t.Lock()
  112. defer t.Unlock()
  113. stat := t.requestsCount[tier]
  114. stat.failure++
  115. t.requestsCount[tier] = stat
  116. }
  117. var (
  118. // {minio_node}_{tier}_{ttlb_seconds_distribution}
  119. tierTTLBMD = MetricDescription{
  120. Namespace: nodeMetricNamespace,
  121. Subsystem: tierSubsystem,
  122. Name: ttlbDistribution,
  123. Help: "Distribution of time to last byte for objects downloaded from warm tier",
  124. Type: gaugeMetric,
  125. }
  126. // {minio_node}_{tier}_{requests_success}
  127. tierRequestsSuccessMD = MetricDescription{
  128. Namespace: nodeMetricNamespace,
  129. Subsystem: tierSubsystem,
  130. Name: tierRequestsSuccess,
  131. Help: "Number of requests to download object from warm tier that were successful",
  132. Type: counterMetric,
  133. }
  134. // {minio_node}_{tier}_{requests_failure}
  135. tierRequestsFailureMD = MetricDescription{
  136. Namespace: nodeMetricNamespace,
  137. Subsystem: tierSubsystem,
  138. Name: tierRequestsFailure,
  139. Help: "Number of requests to download object from warm tier that failed",
  140. Type: counterMetric,
  141. }
  142. )
  143. func (t *tierMetrics) Report() []MetricV2 {
  144. metrics := getHistogramMetrics(t.histogram, tierTTLBMD, true, true)
  145. t.RLock()
  146. defer t.RUnlock()
  147. for tier, stat := range t.requestsCount {
  148. metrics = append(metrics, MetricV2{
  149. Description: tierRequestsSuccessMD,
  150. Value: float64(stat.success),
  151. VariableLabels: map[string]string{"tier": tier},
  152. })
  153. metrics = append(metrics, MetricV2{
  154. Description: tierRequestsFailureMD,
  155. Value: float64(stat.failure),
  156. VariableLabels: map[string]string{"tier": tier},
  157. })
  158. }
  159. return metrics
  160. }
  161. func (config *TierConfigMgr) refreshedAt() time.Time {
  162. config.RLock()
  163. defer config.RUnlock()
  164. return config.lastRefreshedAt
  165. }
  166. // IsTierValid returns true if there exists a remote tier by name tierName,
  167. // otherwise returns false.
  168. func (config *TierConfigMgr) IsTierValid(tierName string) bool {
  169. config.RLock()
  170. defer config.RUnlock()
  171. _, valid := config.isTierNameInUse(tierName)
  172. return valid
  173. }
  174. // isTierNameInUse returns tier type and true if there exists a remote tier by
  175. // name tierName, otherwise returns madmin.Unsupported and false. N B this
  176. // function is meant for internal use, where the caller is expected to take
  177. // appropriate locks.
  178. func (config *TierConfigMgr) isTierNameInUse(tierName string) (madmin.TierType, bool) {
  179. if t, ok := config.Tiers[tierName]; ok {
  180. return t.Type, true
  181. }
  182. return madmin.Unsupported, false
  183. }
  184. // Add adds tier to config if it passes all validations.
  185. func (config *TierConfigMgr) Add(ctx context.Context, tier madmin.TierConfig, ignoreInUse bool) error {
  186. config.Lock()
  187. defer config.Unlock()
  188. // check if tier name is in all caps
  189. tierName := tier.Name
  190. if tierName != strings.ToUpper(tierName) {
  191. return errTierNameNotUppercase
  192. }
  193. // check if tier name already in use
  194. if _, exists := config.isTierNameInUse(tierName); exists {
  195. return errTierAlreadyExists
  196. }
  197. d, err := newWarmBackend(ctx, tier, true)
  198. if err != nil {
  199. return err
  200. }
  201. if !ignoreInUse {
  202. // Check if warmbackend is in use by other MinIO tenants
  203. inUse, err := d.InUse(ctx)
  204. if err != nil {
  205. return err
  206. }
  207. if inUse {
  208. return errTierBackendInUse
  209. }
  210. }
  211. config.Tiers[tierName] = tier
  212. config.drivercache[tierName] = d
  213. return nil
  214. }
  215. // Remove removes tier if it is empty.
  216. func (config *TierConfigMgr) Remove(ctx context.Context, tier string, force bool) error {
  217. d, err := config.getDriver(ctx, tier)
  218. if err != nil {
  219. if errors.Is(err, errTierNotFound) {
  220. return nil
  221. }
  222. return err
  223. }
  224. if !force {
  225. if inuse, err := d.InUse(ctx); err != nil {
  226. return err
  227. } else if inuse {
  228. return errTierBackendNotEmpty
  229. }
  230. }
  231. config.Lock()
  232. delete(config.Tiers, tier)
  233. delete(config.drivercache, tier)
  234. config.Unlock()
  235. return nil
  236. }
  237. // Verify verifies if tier's config is valid by performing all supported
  238. // operations on the corresponding warmbackend.
  239. func (config *TierConfigMgr) Verify(ctx context.Context, tier string) error {
  240. d, err := config.getDriver(ctx, tier)
  241. if err != nil {
  242. return err
  243. }
  244. return checkWarmBackend(ctx, d)
  245. }
  246. // Empty returns if tier targets are empty
  247. func (config *TierConfigMgr) Empty() bool {
  248. if config == nil {
  249. return true
  250. }
  251. return len(config.ListTiers()) == 0
  252. }
  253. // TierType returns the type of tier
  254. func (config *TierConfigMgr) TierType(name string) string {
  255. config.RLock()
  256. defer config.RUnlock()
  257. cfg, ok := config.Tiers[name]
  258. if !ok {
  259. return "internal"
  260. }
  261. return cfg.Type.String()
  262. }
  263. // ListTiers lists remote tiers configured in this deployment.
  264. func (config *TierConfigMgr) ListTiers() []madmin.TierConfig {
  265. if config == nil {
  266. return nil
  267. }
  268. config.RLock()
  269. defer config.RUnlock()
  270. var tierCfgs []madmin.TierConfig
  271. for _, tier := range config.Tiers {
  272. // This makes a local copy of tier config before
  273. // passing a reference to it.
  274. tier := tier.Clone()
  275. tierCfgs = append(tierCfgs, tier)
  276. }
  277. return tierCfgs
  278. }
  279. // Edit replaces the credentials of the remote tier specified by tierName with creds.
  280. func (config *TierConfigMgr) Edit(ctx context.Context, tierName string, creds madmin.TierCreds) error {
  281. config.Lock()
  282. defer config.Unlock()
  283. // check if tier by this name exists
  284. tierType, exists := config.isTierNameInUse(tierName)
  285. if !exists {
  286. return errTierNotFound
  287. }
  288. cfg := config.Tiers[tierName]
  289. switch tierType {
  290. case madmin.S3:
  291. if creds.AWSRole {
  292. cfg.S3.AWSRole = true
  293. }
  294. if creds.AWSRoleWebIdentityTokenFile != "" && creds.AWSRoleARN != "" {
  295. cfg.S3.AWSRoleARN = creds.AWSRoleARN
  296. cfg.S3.AWSRoleWebIdentityTokenFile = creds.AWSRoleWebIdentityTokenFile
  297. }
  298. if creds.AccessKey != "" && creds.SecretKey != "" {
  299. cfg.S3.AccessKey = creds.AccessKey
  300. cfg.S3.SecretKey = creds.SecretKey
  301. }
  302. case madmin.Azure:
  303. if creds.SecretKey != "" {
  304. cfg.Azure.AccountKey = creds.SecretKey
  305. }
  306. if creds.AzSP.TenantID != "" {
  307. cfg.Azure.SPAuth.TenantID = creds.AzSP.TenantID
  308. }
  309. if creds.AzSP.ClientID != "" {
  310. cfg.Azure.SPAuth.ClientID = creds.AzSP.ClientID
  311. }
  312. if creds.AzSP.ClientSecret != "" {
  313. cfg.Azure.SPAuth.ClientSecret = creds.AzSP.ClientSecret
  314. }
  315. case madmin.GCS:
  316. if creds.CredsJSON == nil {
  317. return errTierMissingCredentials
  318. }
  319. cfg.GCS.Creds = base64.URLEncoding.EncodeToString(creds.CredsJSON)
  320. case madmin.MinIO:
  321. if creds.AccessKey == "" || creds.SecretKey == "" {
  322. return errTierMissingCredentials
  323. }
  324. cfg.MinIO.AccessKey = creds.AccessKey
  325. cfg.MinIO.SecretKey = creds.SecretKey
  326. }
  327. d, err := newWarmBackend(ctx, cfg, true)
  328. if err != nil {
  329. return err
  330. }
  331. config.Tiers[tierName] = cfg
  332. config.drivercache[tierName] = d
  333. return nil
  334. }
  335. // Bytes returns msgpack encoded config with format and version headers.
  336. func (config *TierConfigMgr) Bytes() ([]byte, error) {
  337. config.RLock()
  338. defer config.RUnlock()
  339. data := make([]byte, 4, config.Msgsize()+4)
  340. // Initialize the header.
  341. binary.LittleEndian.PutUint16(data[0:2], tierConfigFormat)
  342. binary.LittleEndian.PutUint16(data[2:4], tierConfigVersion)
  343. // Marshal the tier config
  344. return config.MarshalMsg(data)
  345. }
  346. // getDriver returns a warmBackend interface object initialized with remote tier config matching tierName
  347. func (config *TierConfigMgr) getDriver(ctx context.Context, tierName string) (d WarmBackend, err error) {
  348. config.Lock()
  349. defer config.Unlock()
  350. var ok bool
  351. // Lookup in-memory drivercache
  352. d, ok = config.drivercache[tierName]
  353. if ok {
  354. return d, nil
  355. }
  356. // Initialize driver from tier config matching tierName
  357. t, ok := config.Tiers[tierName]
  358. if !ok {
  359. return nil, errTierNotFound
  360. }
  361. d, err = newWarmBackend(ctx, t, false)
  362. if err != nil {
  363. return nil, err
  364. }
  365. config.drivercache[tierName] = d
  366. return d, nil
  367. }
  368. // configReader returns a PutObjReader and ObjectOptions needed to save config
  369. // using a PutObject API. PutObjReader encrypts json encoded tier configurations
  370. // if KMS is enabled, otherwise simply yields the json encoded bytes as is.
  371. // Similarly, ObjectOptions value depends on KMS' status.
  372. func (config *TierConfigMgr) configReader(ctx context.Context) (*PutObjReader, *ObjectOptions, error) {
  373. b, err := config.Bytes()
  374. if err != nil {
  375. return nil, nil, err
  376. }
  377. payloadSize := int64(len(b))
  378. br := bytes.NewReader(b)
  379. hr, err := hash.NewReader(ctx, br, payloadSize, "", "", payloadSize)
  380. if err != nil {
  381. return nil, nil, err
  382. }
  383. if GlobalKMS == nil {
  384. return NewPutObjReader(hr), &ObjectOptions{MaxParity: true}, nil
  385. }
  386. // Note: Local variables with names ek, oek, etc are named inline with
  387. // acronyms defined here -
  388. // https://github.com/minio/minio/blob/master/docs/security/README.md#acronyms
  389. // Encrypt json encoded tier configurations
  390. metadata := make(map[string]string)
  391. encBr, oek, err := newEncryptReader(context.Background(), hr, crypto.S3, "", nil, minioMetaBucket, tierConfigPath, metadata, kms.Context{})
  392. if err != nil {
  393. return nil, nil, err
  394. }
  395. info := ObjectInfo{
  396. Size: payloadSize,
  397. }
  398. encSize := info.EncryptedSize()
  399. encHr, err := hash.NewReader(ctx, encBr, encSize, "", "", encSize)
  400. if err != nil {
  401. return nil, nil, err
  402. }
  403. pReader, err := NewPutObjReader(hr).WithEncryption(encHr, &oek)
  404. if err != nil {
  405. return nil, nil, err
  406. }
  407. opts := &ObjectOptions{
  408. UserDefined: metadata,
  409. MTime: UTCNow(),
  410. MaxParity: true,
  411. }
  412. return pReader, opts, nil
  413. }
  414. // Reload updates config by reloading remote tier config from config store.
  415. func (config *TierConfigMgr) Reload(ctx context.Context, objAPI ObjectLayer) error {
  416. newConfig, err := loadTierConfig(ctx, objAPI)
  417. config.Lock()
  418. defer config.Unlock()
  419. switch err {
  420. case nil:
  421. break
  422. case errConfigNotFound: // nothing to reload
  423. // To maintain the invariance that lastRefreshedAt records the
  424. // timestamp of last successful refresh
  425. config.lastRefreshedAt = UTCNow()
  426. return nil
  427. default:
  428. return err
  429. }
  430. // Reset drivercache built using current config
  431. clear(config.drivercache)
  432. // Remove existing tier configs
  433. clear(config.Tiers)
  434. // Copy over the new tier configs
  435. for tier, cfg := range newConfig.Tiers {
  436. config.Tiers[tier] = cfg
  437. }
  438. config.lastRefreshedAt = UTCNow()
  439. return nil
  440. }
  441. // Save saves tier configuration onto objAPI
  442. func (config *TierConfigMgr) Save(ctx context.Context, objAPI ObjectLayer) error {
  443. if objAPI == nil {
  444. return errServerNotInitialized
  445. }
  446. pr, opts, err := globalTierConfigMgr.configReader(ctx)
  447. if err != nil {
  448. return err
  449. }
  450. _, err = objAPI.PutObject(ctx, minioMetaBucket, tierConfigPath, pr, *opts)
  451. return err
  452. }
  453. // NewTierConfigMgr - creates new tier configuration manager,
  454. func NewTierConfigMgr() *TierConfigMgr {
  455. return &TierConfigMgr{
  456. drivercache: make(map[string]WarmBackend),
  457. Tiers: make(map[string]madmin.TierConfig),
  458. }
  459. }
  460. func (config *TierConfigMgr) refreshTierConfig(ctx context.Context, objAPI ObjectLayer) {
  461. const tierCfgRefresh = 15 * time.Minute
  462. r := rand.New(rand.NewSource(time.Now().UnixNano()))
  463. randInterval := func() time.Duration {
  464. return time.Duration(r.Float64() * 5 * float64(time.Second))
  465. }
  466. // To avoid all MinIO nodes reading the tier config object at the same
  467. // time.
  468. t := time.NewTimer(tierCfgRefresh + randInterval())
  469. defer t.Stop()
  470. for {
  471. select {
  472. case <-ctx.Done():
  473. return
  474. case <-t.C:
  475. err := config.Reload(ctx, objAPI)
  476. if err != nil {
  477. tierLogIf(ctx, err)
  478. }
  479. }
  480. t.Reset(tierCfgRefresh + randInterval())
  481. }
  482. }
  483. // loadTierConfig loads remote tier configuration from objAPI.
  484. func loadTierConfig(ctx context.Context, objAPI ObjectLayer) (*TierConfigMgr, error) {
  485. if objAPI == nil {
  486. return nil, errServerNotInitialized
  487. }
  488. data, err := readConfig(ctx, objAPI, tierConfigPath)
  489. if err != nil {
  490. return nil, err
  491. }
  492. if len(data) <= 4 {
  493. return nil, errors.New("tierConfigInit: no data")
  494. }
  495. // Read header
  496. switch format := binary.LittleEndian.Uint16(data[0:2]); format {
  497. case tierConfigFormat:
  498. default:
  499. return nil, fmt.Errorf("tierConfigInit: unknown format: %d", format)
  500. }
  501. cfg := NewTierConfigMgr()
  502. switch version := binary.LittleEndian.Uint16(data[2:4]); version {
  503. case tierConfigV1, tierConfigVersion:
  504. if _, decErr := cfg.UnmarshalMsg(data[4:]); decErr != nil {
  505. return nil, decErr
  506. }
  507. default:
  508. return nil, fmt.Errorf("tierConfigInit: unknown version: %d", version)
  509. }
  510. return cfg, nil
  511. }
  512. // Init initializes tier configuration reading from objAPI
  513. func (config *TierConfigMgr) Init(ctx context.Context, objAPI ObjectLayer) error {
  514. err := config.Reload(ctx, objAPI)
  515. if globalIsDistErasure {
  516. go config.refreshTierConfig(ctx, objAPI)
  517. }
  518. return err
  519. }