2021-04-18 15:41:13 -04:00
// Copyright (c) 2015-2021 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 <http://www.gnu.org/licenses/>.
2018-02-15 20:45:57 -05:00
package cmd
import (
2023-12-07 04:33:56 -05:00
"errors"
2018-02-15 20:45:57 -05:00
"fmt"
2023-12-07 04:33:56 -05:00
"net/url"
2023-01-23 05:54:50 -05:00
"runtime"
2020-04-27 17:39:57 -04:00
"sort"
2018-08-15 19:35:21 -04:00
"strconv"
2018-02-15 20:45:57 -05:00
"strings"
2023-12-07 04:33:56 -05:00
"github.com/cespare/xxhash/v2"
2020-07-14 12:38:05 -04:00
"github.com/minio/minio-go/v7/pkg/set"
2021-06-01 17:59:40 -04:00
"github.com/minio/minio/internal/config"
2023-09-04 15:57:37 -04:00
"github.com/minio/pkg/v2/ellipses"
"github.com/minio/pkg/v2/env"
2018-02-15 20:45:57 -05:00
)
// This file implements and supports ellipses pattern for
// `minio server` command line arguments.
// Endpoint set represents parsed ellipses values, also provides
// methods to get the sets of endpoints.
type endpointSet struct {
argPatterns [ ] ellipses . ArgPattern
endpoints [ ] string // Endpoints saved from previous GetEndpoints().
setIndexes [ ] [ ] uint64 // All the sets.
}
// Supported set sizes this is used to find the optimal
// single set size.
2022-06-27 23:22:18 -04:00
var setSizes = [ ] uint64 { 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 , 11 , 12 , 13 , 14 , 15 , 16 }
2018-02-15 20:45:57 -05:00
// getDivisibleSize - returns a greatest common divisor of
// all the ellipses sizes.
func getDivisibleSize ( totalSizes [ ] uint64 ) ( result uint64 ) {
gcd := func ( x , y uint64 ) uint64 {
for y != 0 {
x , y = y , x % y
}
return x
}
result = totalSizes [ 0 ]
for i := 1 ; i < len ( totalSizes ) ; i ++ {
result = gcd ( result , totalSizes [ i ] )
}
return result
}
2019-11-19 20:42:27 -05:00
// isValidSetSize - checks whether given count is a valid set size for erasure coding.
var isValidSetSize = func ( count uint64 ) bool {
2020-03-31 12:32:16 -04:00
return ( count >= setSizes [ 0 ] && count <= setSizes [ len ( setSizes ) - 1 ] )
2019-11-19 20:42:27 -05:00
}
2020-04-27 17:39:57 -04:00
func commonSetDriveCount ( divisibleSize uint64 , setCounts [ ] uint64 ) ( setSize uint64 ) {
// prefers setCounts to be sorted for optimal behavior.
if divisibleSize < setCounts [ len ( setCounts ) - 1 ] {
return divisibleSize
}
// Figure out largest value of total_drives_in_erasure_set which results
// in least number of total_drives/total_drives_erasure_set ratio.
prevD := divisibleSize / setCounts [ 0 ]
for _ , cnt := range setCounts {
if divisibleSize % cnt == 0 {
d := divisibleSize / cnt
if d <= prevD {
prevD = d
setSize = cnt
}
}
}
return setSize
}
// possibleSetCountsWithSymmetry returns symmetrical setCounts based on the
// input argument patterns, the symmetry calculation is to ensure that
// we also use uniform number of drives common across all ellipses patterns.
func possibleSetCountsWithSymmetry ( setCounts [ ] uint64 , argPatterns [ ] ellipses . ArgPattern ) [ ] uint64 {
2022-01-02 12:15:06 -05:00
newSetCounts := make ( map [ uint64 ] struct { } )
2020-04-27 17:39:57 -04:00
for _ , ss := range setCounts {
var symmetry bool
for _ , argPattern := range argPatterns {
for _ , p := range argPattern {
if uint64 ( len ( p . Seq ) ) > ss {
symmetry = uint64 ( len ( p . Seq ) ) % ss == 0
} else {
symmetry = ss % uint64 ( len ( p . Seq ) ) == 0
}
}
}
// With no arg patterns, it is expected that user knows
// the right symmetry, so either ellipses patterns are
// provided (recommended) or no ellipses patterns.
if _ , ok := newSetCounts [ ss ] ; ! ok && ( symmetry || argPatterns == nil ) {
newSetCounts [ ss ] = struct { } { }
}
}
setCounts = [ ] uint64 { }
for setCount := range newSetCounts {
setCounts = append ( setCounts , setCount )
}
// Not necessarily needed but it ensures to the readers
// eyes that we prefer a sorted setCount slice for the
// subsequent function to figure out the right common
// divisor, it avoids loops.
sort . Slice ( setCounts , func ( i , j int ) bool {
return setCounts [ i ] < setCounts [ j ]
} )
return setCounts
}
2018-02-15 20:45:57 -05:00
// getSetIndexes returns list of indexes which provides the set size
// on each index, this function also determines the final set size
// The final set size has the affinity towards choosing smaller
// indexes (total sets)
2020-04-27 17:39:57 -04:00
func getSetIndexes ( args [ ] string , totalSizes [ ] uint64 , customSetDriveCount uint64 , argPatterns [ ] ellipses . ArgPattern ) ( setIndexes [ ] [ ] uint64 , err error ) {
2018-02-15 20:45:57 -05:00
if len ( totalSizes ) == 0 || len ( args ) == 0 {
return nil , errInvalidArgument
}
setIndexes = make ( [ ] [ ] uint64 , len ( totalSizes ) )
2018-05-08 22:04:36 -04:00
for _ , totalSize := range totalSizes {
2018-02-15 20:45:57 -05:00
// Check if totalSize has minimum range upto setSize
2018-08-15 19:35:21 -04:00
if totalSize < setSizes [ 0 ] || totalSize < customSetDriveCount {
2020-01-07 12:13:44 -05:00
msg := fmt . Sprintf ( "Incorrect number of endpoints provided %s" , args )
return nil , config . ErrInvalidNumberOfErasureEndpoints ( nil ) . Msg ( msg )
2018-02-15 20:45:57 -05:00
}
}
commonSize := getDivisibleSize ( totalSizes )
2018-08-15 19:35:21 -04:00
possibleSetCounts := func ( setSize uint64 ) ( ss [ ] uint64 ) {
for _ , s := range setSizes {
if setSize % s == 0 {
ss = append ( ss , s )
}
}
return ss
}
2020-03-08 16:30:25 -04:00
setCounts := possibleSetCounts ( commonSize )
if len ( setCounts ) == 0 {
2022-08-04 19:10:08 -04:00
msg := fmt . Sprintf ( "Incorrect number of endpoints provided %s, number of drives %d is not divisible by any supported erasure set sizes %d" , args , commonSize , setSizes )
2020-03-08 16:30:25 -04:00
return nil , config . ErrInvalidNumberOfErasureEndpoints ( nil ) . Msg ( msg )
}
2020-04-27 17:39:57 -04:00
var setSize uint64
// Custom set drive count allows to override automatic distribution.
// only meant if you want to further optimize drive distribution.
2018-08-15 19:35:21 -04:00
if customSetDriveCount > 0 {
2020-03-08 16:30:25 -04:00
msg := fmt . Sprintf ( "Invalid set drive count. Acceptable values for %d number drives are %d" , commonSize , setCounts )
var found bool
for _ , ss := range setCounts {
if ss == customSetDriveCount {
found = true
}
}
if ! found {
2019-10-04 13:35:33 -04:00
return nil , config . ErrInvalidErasureSetSize ( nil ) . Msg ( msg )
2018-08-15 19:35:21 -04:00
}
2020-04-27 17:39:57 -04:00
// No automatic symmetry calculation expected, user is on their own
2018-08-15 19:35:21 -04:00
setSize = customSetDriveCount
2020-04-27 17:39:57 -04:00
globalCustomErasureDriveCount = true
} else {
// Returns possible set counts with symmetry.
setCounts = possibleSetCountsWithSymmetry ( setCounts , argPatterns )
2020-06-07 01:13:48 -04:00
if len ( setCounts ) == 0 {
2022-08-04 19:10:08 -04:00
msg := fmt . Sprintf ( "No symmetric distribution detected with input endpoints provided %s, drives %d cannot be spread symmetrically by any supported erasure set sizes %d" , args , commonSize , setSizes )
2020-06-07 01:13:48 -04:00
return nil , config . ErrInvalidNumberOfErasureEndpoints ( nil ) . Msg ( msg )
}
2020-04-27 17:39:57 -04:00
// Final set size with all the symmetry accounted for.
setSize = commonSetDriveCount ( commonSize , setCounts )
2018-02-15 20:45:57 -05:00
}
// Check whether setSize is with the supported range.
if ! isValidSetSize ( setSize ) {
2022-08-04 19:10:08 -04:00
msg := fmt . Sprintf ( "Incorrect number of endpoints provided %s, number of drives %d is not divisible by any supported erasure set sizes %d" , args , commonSize , setSizes )
2020-01-07 12:13:44 -05:00
return nil , config . ErrInvalidNumberOfErasureEndpoints ( nil ) . Msg ( msg )
2018-02-15 20:45:57 -05:00
}
for i := range totalSizes {
for j := uint64 ( 0 ) ; j < totalSizes [ i ] / setSize ; j ++ {
setIndexes [ i ] = append ( setIndexes [ i ] , setSize )
}
}
return setIndexes , nil
}
// Returns all the expanded endpoints, each argument is expanded separately.
2022-12-05 14:18:50 -05:00
func ( s * endpointSet ) getEndpoints ( ) ( endpoints [ ] string ) {
2018-02-15 20:45:57 -05:00
if len ( s . endpoints ) != 0 {
return s . endpoints
}
for _ , argPattern := range s . argPatterns {
for _ , lbls := range argPattern . Expand ( ) {
endpoints = append ( endpoints , strings . Join ( lbls , "" ) )
}
}
s . endpoints = endpoints
return endpoints
}
// Get returns the sets representation of the endpoints
// this function also intelligently decides on what will
// be the right set size etc.
func ( s endpointSet ) Get ( ) ( sets [ ] [ ] string ) {
2022-01-02 12:15:06 -05:00
k := uint64 ( 0 )
2018-02-15 20:45:57 -05:00
endpoints := s . getEndpoints ( )
for i := range s . setIndexes {
for j := range s . setIndexes [ i ] {
sets = append ( sets , endpoints [ k : s . setIndexes [ i ] [ j ] + k ] )
k = s . setIndexes [ i ] [ j ] + k
}
}
return sets
}
// Return the total size for each argument patterns.
func getTotalSizes ( argPatterns [ ] ellipses . ArgPattern ) [ ] uint64 {
var totalSizes [ ] uint64
for _ , argPattern := range argPatterns {
var totalSize uint64 = 1
for _ , p := range argPattern {
2021-11-16 12:28:29 -05:00
totalSize *= uint64 ( len ( p . Seq ) )
2018-02-15 20:45:57 -05:00
}
totalSizes = append ( totalSizes , totalSize )
}
return totalSizes
}
// Parses all arguments and returns an endpointSet which is a collection
// of endpoints following the ellipses pattern, this is what is used
// by the object layer for initializing itself.
2019-11-19 20:42:27 -05:00
func parseEndpointSet ( customSetDriveCount uint64 , args ... string ) ( ep endpointSet , err error ) {
2022-01-02 12:15:06 -05:00
argPatterns := make ( [ ] ellipses . ArgPattern , len ( args ) )
2018-02-15 20:45:57 -05:00
for i , arg := range args {
2018-02-28 17:30:00 -05:00
patterns , perr := ellipses . FindEllipsesPatterns ( arg )
if perr != nil {
2019-10-04 13:35:33 -04:00
return endpointSet { } , config . ErrInvalidErasureEndpoints ( nil ) . Msg ( perr . Error ( ) )
2018-02-15 20:45:57 -05:00
}
argPatterns [ i ] = patterns
}
2020-04-27 17:39:57 -04:00
ep . setIndexes , err = getSetIndexes ( args , getTotalSizes ( argPatterns ) , customSetDriveCount , argPatterns )
2018-02-15 20:45:57 -05:00
if err != nil {
2019-10-04 13:35:33 -04:00
return endpointSet { } , config . ErrInvalidErasureEndpoints ( nil ) . Msg ( err . Error ( ) )
2018-02-15 20:45:57 -05:00
}
ep . argPatterns = argPatterns
return ep , nil
}
2019-06-10 10:57:42 -04:00
// GetAllSets - parses all ellipses input arguments, expands them into
// corresponding list of endpoints chunked evenly in accordance with a
// specific set size.
2018-02-15 20:45:57 -05:00
// For example: {1...64} is divided into 4 sets each of size 16.
// This applies to even distributed setup syntax as well.
2021-01-16 15:08:02 -05:00
func GetAllSets ( args ... string ) ( [ ] [ ] string , error ) {
var customSetDriveCount uint64
if v := env . Get ( EnvErasureSetDriveCount , "" ) ; v != "" {
driveCount , err := strconv . Atoi ( v )
if err != nil {
return nil , config . ErrInvalidErasureSetSize ( err )
}
customSetDriveCount = uint64 ( driveCount )
}
2018-02-15 20:45:57 -05:00
var setArgs [ ] [ ] string
if ! ellipses . HasEllipses ( args ... ) {
var setIndexes [ ] [ ] uint64
// Check if we have more one args.
if len ( args ) > 1 {
var err error
2020-04-27 17:39:57 -04:00
setIndexes , err = getSetIndexes ( args , [ ] uint64 { uint64 ( len ( args ) ) } , customSetDriveCount , nil )
2018-02-15 20:45:57 -05:00
if err != nil {
return nil , err
}
} else {
// We are in FS setup, proceed forward.
2018-02-26 13:22:03 -05:00
setIndexes = [ ] [ ] uint64 { { uint64 ( len ( args ) ) } }
2018-02-15 20:45:57 -05:00
}
s := endpointSet {
endpoints : args ,
setIndexes : setIndexes ,
}
setArgs = s . Get ( )
} else {
2019-11-19 20:42:27 -05:00
s , err := parseEndpointSet ( customSetDriveCount , args ... )
2018-02-15 20:45:57 -05:00
if err != nil {
return nil , err
}
setArgs = s . Get ( )
}
uniqueArgs := set . NewStringSet ( )
for _ , sargs := range setArgs {
for _ , arg := range sargs {
if uniqueArgs . Contains ( arg ) {
2019-10-04 13:35:33 -04:00
return nil , config . ErrInvalidErasureEndpoints ( nil ) . Msg ( fmt . Sprintf ( "Input args (%s) has duplicate ellipses" , args ) )
2018-02-15 20:45:57 -05:00
}
uniqueArgs . Add ( arg )
}
}
return setArgs , nil
}
2020-04-27 17:39:57 -04:00
// Override set drive count for manual distribution.
const (
EnvErasureSetDriveCount = "MINIO_ERASURE_SET_DRIVE_COUNT"
)
2022-01-02 12:15:06 -05:00
var globalCustomErasureDriveCount = false
2020-04-27 17:39:57 -04:00
2023-12-07 04:33:56 -05:00
type node struct {
nodeName string
disks [ ] string
}
type endpointsList [ ] node
func ( el * endpointsList ) add ( arg string ) error {
u , err := url . Parse ( arg )
if err != nil {
return err
}
found := false
list := * el
for i := range list {
if list [ i ] . nodeName == u . Host {
list [ i ] . disks = append ( list [ i ] . disks , u . String ( ) )
found = true
break
}
}
if ! found {
list = append ( list , node { nodeName : u . Host , disks : [ ] string { u . String ( ) } } )
}
* el = list
return nil
}
// buildDisksLayoutFromConfFile supports with and without ellipses transparently.
func buildDisksLayoutFromConfFile ( pools [ ] [ ] string ) ( layout disksLayout , err error ) {
if len ( pools ) == 0 {
return layout , errInvalidArgument
}
for _ , list := range pools {
var endpointsList endpointsList
for _ , arg := range list {
switch {
case ellipses . HasList ( arg ) :
patterns , err := ellipses . FindListPatterns ( arg )
if err != nil {
return layout , err
}
for _ , exp := range patterns . Expand ( ) {
for _ , ep := range exp {
if err := endpointsList . add ( ep ) ; err != nil {
return layout , err
}
}
}
case ellipses . HasEllipses ( arg ) :
patterns , err := ellipses . FindEllipsesPatterns ( arg )
if err != nil {
return layout , err
}
for _ , exp := range patterns . Expand ( ) {
if err := endpointsList . add ( strings . Join ( exp , "" ) ) ; err != nil {
return layout , err
}
}
default :
if err := endpointsList . add ( arg ) ; err != nil {
return layout , err
}
}
}
var stopping bool
var singleNode bool
var eps [ ] string
for i := 0 ; ; i ++ {
for _ , node := range endpointsList {
if node . nodeName == "" {
singleNode = true
}
if len ( node . disks ) <= i {
stopping = true
continue
}
if stopping {
return layout , errors . New ( "number of disks per node does not match" )
}
eps = append ( eps , node . disks [ i ] )
}
if stopping {
break
}
}
for _ , node := range endpointsList {
if node . nodeName != "" && singleNode {
return layout , errors . New ( "all arguments must but either single node or distributed" )
}
}
setArgs , err := GetAllSets ( eps ... )
if err != nil {
return layout , err
}
h := xxhash . New ( )
for _ , s := range setArgs {
for _ , d := range s {
h . WriteString ( d )
}
}
layout . pools = append ( layout . pools , poolDisksLayout {
cmdline : fmt . Sprintf ( "hash:%x" , h . Sum ( nil ) ) ,
layout : setArgs ,
} )
}
return
}
// mergeDisksLayoutFromArgs supports with and without ellipses transparently.
func mergeDisksLayoutFromArgs ( args [ ] string , ctxt * serverCtxt ) ( err error ) {
2019-11-19 20:42:27 -05:00
if len ( args ) == 0 {
2023-12-07 04:33:56 -05:00
return errInvalidArgument
2018-02-15 20:45:57 -05:00
}
2022-11-01 19:41:01 -04:00
ok := true
for _ , arg := range args {
ok = ok && ! ellipses . HasEllipses ( arg )
}
2023-12-07 04:33:56 -05:00
var setArgs [ ] [ ] string
2022-11-01 19:41:01 -04:00
// None of the args have ellipses use the old style.
if ok {
2023-12-07 04:33:56 -05:00
setArgs , err = GetAllSets ( args ... )
2019-11-19 20:42:27 -05:00
if err != nil {
2023-12-07 04:33:56 -05:00
return err
2019-11-19 20:42:27 -05:00
}
2023-12-07 04:33:56 -05:00
ctxt . Layout = disksLayout {
legacy : true ,
2024-04-11 17:22:47 -04:00
pools : [ ] poolDisksLayout { { layout : setArgs , cmdline : strings . Join ( args , " " ) } } ,
2023-06-23 14:48:23 -04:00
}
2023-12-07 04:33:56 -05:00
return
2018-02-15 20:45:57 -05:00
}
2019-11-19 20:42:27 -05:00
for _ , arg := range args {
2022-11-01 19:41:01 -04:00
if ! ellipses . HasEllipses ( arg ) && len ( args ) > 1 {
// TODO: support SNSD deployments to be decommissioned in future
2023-12-07 04:33:56 -05:00
return fmt . Errorf ( "all args must have ellipses for pool expansion (%w) args: %s" , errInvalidArgument , args )
2022-11-01 19:41:01 -04:00
}
2023-12-07 04:33:56 -05:00
setArgs , err = GetAllSets ( arg )
2019-11-19 20:42:27 -05:00
if err != nil {
2023-12-07 04:33:56 -05:00
return err
2019-11-19 20:42:27 -05:00
}
2023-12-07 04:33:56 -05:00
ctxt . Layout . pools = append ( ctxt . Layout . pools , poolDisksLayout { cmdline : arg , layout : setArgs } )
}
return
}
2021-01-16 15:08:02 -05:00
2023-12-07 04:33:56 -05:00
// CreateServerEndpoints - validates and creates new endpoints from input args, supports
// both ellipses and without ellipses transparently.
func createServerEndpoints ( serverAddr string , poolArgs [ ] poolDisksLayout , legacy bool ) (
endpointServerPools EndpointServerPools , setupType SetupType , err error ,
) {
if len ( poolArgs ) == 0 {
return nil , - 1 , errInvalidArgument
2023-05-11 20:41:33 -04:00
}
poolEndpoints , setupType , err := CreatePoolEndpoints ( serverAddr , poolArgs ... )
if err != nil {
return nil , - 1 , err
}
for i , endpointList := range poolEndpoints {
2021-01-26 23:47:42 -05:00
if err = endpointServerPools . Add ( PoolEndpoints {
2023-12-07 04:33:56 -05:00
Legacy : legacy ,
SetCount : len ( poolArgs [ i ] . layout ) ,
DrivesPerSet : len ( poolArgs [ i ] . layout [ 0 ] ) ,
2019-11-19 20:42:27 -05:00
Endpoints : endpointList ,
2023-01-23 05:54:50 -05:00
Platform : fmt . Sprintf ( "OS: %s | Arch: %s" , runtime . GOOS , runtime . GOARCH ) ,
2023-12-07 04:33:56 -05:00
CmdLine : poolArgs [ i ] . cmdline ,
2020-01-07 12:13:44 -05:00
} ) ; err != nil {
2020-08-05 16:31:12 -04:00
return nil , - 1 , err
2020-01-07 12:13:44 -05:00
}
2019-11-19 20:42:27 -05:00
}
2019-11-26 14:42:10 -05:00
2020-12-01 16:50:33 -05:00
return endpointServerPools , setupType , nil
2018-02-15 20:45:57 -05:00
}