github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/constraints.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package storage 5 6 import ( 7 "fmt" 8 "regexp" 9 "strconv" 10 "strings" 11 12 "github.com/juju/errors" 13 "github.com/juju/utils/v3" 14 ) 15 16 // Constraints describes a set of storage constraints. 17 type Constraints struct { 18 // Pool is the name of the storage pool (ebs, ceph, custompool, ...) 19 // that must provide the storage, or "" if the default pool should be 20 // used. 21 Pool string 22 23 // Size is the minimum size of the storage in MiB. 24 Size uint64 25 26 // Count is the number of instances of the storage to create. 27 Count uint64 28 } 29 30 var ( 31 poolRE = regexp.MustCompile("^[a-zA-Z]+[-?a-zA-Z0-9]*$") 32 countRE = regexp.MustCompile("^-?[0-9]+$") 33 sizeRE = regexp.MustCompile("^-?[0-9]+(?:\\.[0-9]+)?[MGTPEZY](?:i?B)?$") 34 ) 35 36 // ParseConstraints parses the specified string and creates a 37 // Constraints structure. 38 // 39 // The acceptable format for storage constraints is a comma separated 40 // sequence of: POOL, COUNT, and SIZE, where 41 // 42 // POOL identifies the storage pool. POOL can be a string 43 // starting with a letter, followed by zero or more digits 44 // or letters optionally separated by hyphens. 45 // 46 // COUNT is a positive integer indicating how many instances 47 // of the storage to create. If unspecified, and SIZE is 48 // specified, COUNT defaults to 1. 49 // 50 // SIZE describes the minimum size of the storage instances to 51 // create. SIZE is a floating point number and multiplier from 52 // the set (M, G, T, P, E, Z, Y), which are all treated as 53 // powers of 1024. 54 func ParseConstraints(s string) (Constraints, error) { 55 var cons Constraints 56 fields := strings.Split(s, ",") 57 for _, field := range fields { 58 if field == "" { 59 continue 60 } 61 if IsValidPoolName(field) { 62 if cons.Pool != "" { 63 return cons, errors.NotValidf("pool name is already set to %q, new value %q", cons.Pool, field) 64 } else { 65 cons.Pool = field 66 } 67 continue 68 } 69 if count, ok, err := parseCount(field); ok { 70 if err != nil { 71 return cons, errors.Annotate(err, "cannot parse count") 72 } 73 cons.Count = count 74 continue 75 } 76 if size, ok, err := parseSize(field); ok { 77 if err != nil { 78 return cons, errors.Annotate(err, "cannot parse size") 79 } 80 cons.Size = size 81 continue 82 } 83 return cons, errors.NotValidf("unrecognized storage constraint %q", field) 84 } 85 if cons.Count == 0 && cons.Size == 0 && cons.Pool == "" { 86 return Constraints{}, errors.New("storage constraints require at least one field to be specified") 87 } 88 if cons.Count == 0 { 89 cons.Count = 1 90 } 91 return cons, nil 92 } 93 94 // IsValidPoolName checks if given string is a valid pool name. 95 func IsValidPoolName(s string) bool { 96 return poolRE.MatchString(s) 97 } 98 99 // ParseConstraintsMap parses string representation of 100 // storage constraints into a map keyed on storage names 101 // with constraints as values. 102 // 103 // Storage constraints may be specified as 104 // 105 // <name>=<constraints> 106 // 107 // or as 108 // 109 // <name> 110 // 111 // where latter is equivalent to <name>=1. 112 // 113 // Duplicate storage names cause an error to be returned. 114 // Constraints presence can be enforced. 115 func ParseConstraintsMap(args []string, mustHaveConstraints bool) (map[string]Constraints, error) { 116 results := make(map[string]Constraints, len(args)) 117 for _, kv := range args { 118 parts := strings.SplitN(kv, "=", -1) 119 name := parts[0] 120 if len(parts) > 2 || len(name) == 0 { 121 return nil, errors.Errorf(`expected "name=constraints" or "name", got %q`, kv) 122 } 123 124 if mustHaveConstraints && len(parts) == 1 { 125 return nil, errors.Errorf(`expected "name=constraints" where "constraints" must be specified, got %q`, kv) 126 } 127 128 if _, exists := results[name]; exists { 129 return nil, errors.Errorf("storage %q specified more than once", name) 130 } 131 consString := "1" 132 if len(parts) > 1 { 133 consString = parts[1] 134 } 135 cons, err := ParseConstraints(consString) 136 if err != nil { 137 return nil, errors.Annotatef(err, "cannot parse constraints for storage %q", name) 138 } 139 140 results[name] = cons 141 } 142 return results, nil 143 } 144 145 func parseCount(s string) (uint64, bool, error) { 146 if !countRE.MatchString(s) { 147 return 0, false, nil 148 } 149 var n uint64 150 var err error 151 if s[0] == '-' { 152 goto bad 153 } 154 n, err = strconv.ParseUint(s, 10, 64) 155 if err != nil { 156 return 0, false, nil 157 } 158 if n > 0 { 159 return n, true, nil 160 } 161 bad: 162 return 0, true, errors.Errorf("count must be greater than zero, got %q", s) 163 } 164 165 func parseSize(s string) (uint64, bool, error) { 166 if !sizeRE.MatchString(s) { 167 return 0, false, nil 168 } 169 size, err := utils.ParseSize(s) 170 if err != nil { 171 return 0, true, err 172 } 173 return size, true, nil 174 } 175 176 // ToString returns a parsable string representation of the storage constraints. 177 func ToString(c Constraints) (string, error) { 178 if c.Pool == "" && c.Size <= 0 && c.Count <= 0 { 179 return "", errors.Errorf("must provide one of pool or size or count") 180 } 181 182 var parts []string 183 if c.Pool != "" { 184 parts = append(parts, c.Pool) 185 } 186 if c.Count > 0 { 187 parts = append(parts, fmt.Sprintf("%d", c.Count)) 188 } 189 if c.Size > 0 { 190 parts = append(parts, fmt.Sprintf("%dM", c.Size)) 191 } 192 return strings.Join(parts, ","), nil 193 }