github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/constraints/constraints.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package constraints 5 6 import ( 7 "fmt" 8 "math" 9 "strconv" 10 "strings" 11 12 "github.com/errgo/errgo" 13 14 "launchpad.net/juju-core/instance" 15 ) 16 17 // Value describes a user's requirements of the hardware on which units 18 // of a service will run. Constraints are used to choose an existing machine 19 // onto which a unit will be deployed, or to provision a new machine if no 20 // existing one satisfies the requirements. 21 type Value struct { 22 23 // Arch, if not nil or empty, indicates that a machine must run the named 24 // architecture. 25 Arch *string `json:"arch,omitempty" yaml:"arch,omitempty"` 26 27 // Container, if not nil, indicates that a machine must be the specified container type. 28 Container *instance.ContainerType `json:"container,omitempty" yaml:"container,omitempty"` 29 30 // CpuCores, if not nil, indicates that a machine must have at least that 31 // number of effective cores available. 32 CpuCores *uint64 `json:"cpu-cores,omitempty" yaml:"cpu-cores,omitempty"` 33 34 // CpuPower, if not nil, indicates that a machine must have at least that 35 // amount of CPU power available, where 100 CpuPower is considered to be 36 // equivalent to 1 Amazon ECU (or, roughly, a single 2007-era Xeon). 37 CpuPower *uint64 `json:"cpu-power,omitempty" yaml:"cpu-power,omitempty"` 38 39 // Mem, if not nil, indicates that a machine must have at least that many 40 // megabytes of RAM. 41 Mem *uint64 `json:"mem,omitempty" yaml:"mem,omitempty"` 42 43 // RootDisk, if not nil, indicates that a machine must have at least 44 // that many megabytes of disk space available in the root disk. In 45 // providers where the root disk is configurable at instance startup 46 // time, an instance with the specified amount of disk space in the OS 47 // disk might be requested. 48 RootDisk *uint64 `json:"root-disk,omitempty" yaml:"root-disk,omitempty"` 49 50 // Tags, if not nil, indicates tags that the machine must have applied to it. 51 // An empty list is treated the same as a nil (unspecified) list, except an 52 // empty list will override any default tags, where a nil list will not. 53 Tags *[]string `json:"tags,omitempty" yaml:"tags,omitempty"` 54 } 55 56 // IsEmpty returns if the given constraints value has no constraints set 57 func IsEmpty(v *Value) bool { 58 return v == nil || 59 v.Arch == nil && 60 v.Container == nil && 61 v.CpuCores == nil && 62 v.CpuPower == nil && 63 v.Mem == nil && 64 v.RootDisk == nil && 65 v.Tags == nil 66 } 67 68 // String expresses a constraints.Value in the language in which it was specified. 69 func (v Value) String() string { 70 var strs []string 71 if v.Arch != nil { 72 strs = append(strs, "arch="+*v.Arch) 73 } 74 if v.Container != nil { 75 strs = append(strs, "container="+string(*v.Container)) 76 } 77 if v.CpuCores != nil { 78 strs = append(strs, "cpu-cores="+uintStr(*v.CpuCores)) 79 } 80 if v.CpuPower != nil { 81 strs = append(strs, "cpu-power="+uintStr(*v.CpuPower)) 82 } 83 if v.Mem != nil { 84 s := uintStr(*v.Mem) 85 if s != "" { 86 s += "M" 87 } 88 strs = append(strs, "mem="+s) 89 } 90 if v.RootDisk != nil { 91 s := uintStr(*v.RootDisk) 92 if s != "" { 93 s += "M" 94 } 95 strs = append(strs, "root-disk="+s) 96 } 97 if v.Tags != nil { 98 s := strings.Join(*v.Tags, ",") 99 strs = append(strs, "tags="+s) 100 } 101 return strings.Join(strs, " ") 102 } 103 104 // WithFallbacks returns a copy of v with nil values taken from v0. 105 func (v Value) WithFallbacks(v0 Value) Value { 106 v1 := v0 107 if v.Arch != nil { 108 v1.Arch = v.Arch 109 } 110 if v.Container != nil { 111 v1.Container = v.Container 112 } 113 if v.CpuCores != nil { 114 v1.CpuCores = v.CpuCores 115 } 116 if v.CpuPower != nil { 117 v1.CpuPower = v.CpuPower 118 } 119 if v.Mem != nil { 120 v1.Mem = v.Mem 121 } 122 if v.RootDisk != nil { 123 v1.RootDisk = v.RootDisk 124 } 125 if v.Tags != nil { 126 v1.Tags = v.Tags 127 } 128 return v1 129 } 130 131 func uintStr(i uint64) string { 132 if i == 0 { 133 return "" 134 } 135 return fmt.Sprintf("%d", i) 136 } 137 138 // Parse constructs a constraints.Value from the supplied arguments, 139 // each of which must contain only spaces and name=value pairs. If any 140 // name is specified more than once, an error is returned. 141 func Parse(args ...string) (Value, error) { 142 cons := Value{} 143 for _, arg := range args { 144 raws := strings.Split(strings.TrimSpace(arg), " ") 145 for _, raw := range raws { 146 if raw == "" { 147 continue 148 } 149 if err := cons.setRaw(raw); err != nil { 150 return Value{}, err 151 } 152 } 153 } 154 return cons, nil 155 } 156 157 // MustParse constructs a constraints.Value from the supplied arguments, 158 // as Parse, but panics on failure. 159 func MustParse(args ...string) Value { 160 v, err := Parse(args...) 161 if err != nil { 162 panic(err) 163 } 164 return v 165 } 166 167 // Constraints implements gnuflag.Value for a Constraints. 168 type ConstraintsValue struct { 169 Target *Value 170 } 171 172 func (v ConstraintsValue) Set(s string) error { 173 cons, err := Parse(s) 174 if err != nil { 175 return err 176 } 177 *v.Target = cons 178 return nil 179 } 180 181 func (v ConstraintsValue) String() string { 182 return v.Target.String() 183 } 184 185 // setRaw interprets a name=value string and sets the supplied value. 186 func (v *Value) setRaw(raw string) error { 187 eq := strings.Index(raw, "=") 188 if eq <= 0 { 189 return fmt.Errorf("malformed constraint %q", raw) 190 } 191 name, str := raw[:eq], raw[eq+1:] 192 var err error 193 switch name { 194 case "arch": 195 err = v.setArch(str) 196 case "container": 197 err = v.setContainer(str) 198 case "cpu-cores": 199 err = v.setCpuCores(str) 200 case "cpu-power": 201 err = v.setCpuPower(str) 202 case "mem": 203 err = v.setMem(str) 204 case "root-disk": 205 err = v.setRootDisk(str) 206 case "tags": 207 err = v.setTags(str) 208 default: 209 return fmt.Errorf("unknown constraint %q", name) 210 } 211 if err != nil { 212 return errgo.Annotatef(err, "bad %q constraint", name) 213 } 214 return nil 215 } 216 217 // SetYAML is required to unmarshall a constraints.Value object 218 // to ensure the container attribute is correctly handled when it is empty. 219 // Because ContainerType is an alias for string, Go's reflect logic used in the 220 // YAML decode determines that *string and *ContainerType are not assignable so 221 // the container value of "" in the YAML is ignored. 222 func (v *Value) SetYAML(tag string, value interface{}) bool { 223 values, ok := value.(map[interface{}]interface{}) 224 if !ok { 225 return false 226 } 227 for k, val := range values { 228 vstr := fmt.Sprintf("%v", val) 229 var err error 230 switch k { 231 case "arch": 232 v.Arch = &vstr 233 case "container": 234 ctype := instance.ContainerType(vstr) 235 v.Container = &ctype 236 case "cpu-cores": 237 v.CpuCores, err = parseUint64(vstr) 238 case "cpu-power": 239 v.CpuPower, err = parseUint64(vstr) 240 case "mem": 241 v.Mem, err = parseUint64(vstr) 242 case "root-disk": 243 v.RootDisk, err = parseUint64(vstr) 244 case "tags": 245 v.Tags, err = parseYamlTags(val) 246 default: 247 return false 248 } 249 if err != nil { 250 return false 251 } 252 } 253 return true 254 } 255 256 func (v *Value) setContainer(str string) error { 257 if v.Container != nil { 258 return fmt.Errorf("already set") 259 } 260 if str == "" { 261 ctype := instance.ContainerType("") 262 v.Container = &ctype 263 } else { 264 ctype, err := instance.ParseContainerTypeOrNone(str) 265 if err != nil { 266 return err 267 } 268 v.Container = &ctype 269 } 270 return nil 271 } 272 273 // HasContainer returns true if the constraints.Value specifies a container. 274 func (v *Value) HasContainer() bool { 275 return v.Container != nil && *v.Container != "" && *v.Container != instance.NONE 276 } 277 278 func (v *Value) setArch(str string) error { 279 if v.Arch != nil { 280 return fmt.Errorf("already set") 281 } 282 switch str { 283 case "": 284 case "amd64", "i386", "arm", "arm64", "ppc64": 285 default: 286 return fmt.Errorf("%q not recognized", str) 287 } 288 v.Arch = &str 289 return nil 290 } 291 292 func (v *Value) setCpuCores(str string) (err error) { 293 if v.CpuCores != nil { 294 return fmt.Errorf("already set") 295 } 296 v.CpuCores, err = parseUint64(str) 297 return 298 } 299 300 func (v *Value) setCpuPower(str string) (err error) { 301 if v.CpuPower != nil { 302 return fmt.Errorf("already set") 303 } 304 v.CpuPower, err = parseUint64(str) 305 return 306 } 307 308 func (v *Value) setMem(str string) (err error) { 309 if v.Mem != nil { 310 return fmt.Errorf("already set") 311 } 312 v.Mem, err = parseSize(str) 313 return 314 } 315 316 func (v *Value) setRootDisk(str string) (err error) { 317 if v.RootDisk != nil { 318 return fmt.Errorf("already set") 319 } 320 v.RootDisk, err = parseSize(str) 321 return 322 } 323 324 func (v *Value) setTags(str string) error { 325 if v.Tags != nil { 326 return fmt.Errorf("already set") 327 } 328 v.Tags = parseTags(str) 329 return nil 330 } 331 332 func parseUint64(str string) (*uint64, error) { 333 var value uint64 334 if str != "" { 335 if val, err := strconv.ParseUint(str, 10, 64); err != nil { 336 return nil, fmt.Errorf("must be a non-negative integer") 337 } else { 338 value = uint64(val) 339 } 340 } 341 return &value, nil 342 } 343 344 func parseSize(str string) (*uint64, error) { 345 var value uint64 346 if str != "" { 347 mult := 1.0 348 if m, ok := mbSuffixes[str[len(str)-1:]]; ok { 349 str = str[:len(str)-1] 350 mult = m 351 } 352 val, err := strconv.ParseFloat(str, 64) 353 if err != nil || val < 0 { 354 return nil, fmt.Errorf("must be a non-negative float with optional M/G/T/P suffix") 355 } 356 val *= mult 357 value = uint64(math.Ceil(val)) 358 } 359 return &value, nil 360 } 361 362 // parseTags returns the tags in the value s. We expect the tags to be comma delimited strings. 363 func parseTags(s string) *[]string { 364 if s == "" { 365 return &[]string{} 366 } 367 t := strings.Split(s, ",") 368 return &t 369 } 370 371 func parseYamlTags(val interface{}) (*[]string, error) { 372 ifcs, ok := val.([]interface{}) 373 if !ok { 374 return nil, fmt.Errorf("unexpected type passed to tags: %T", val) 375 } 376 tags := make([]string, len(ifcs)) 377 for n, ifc := range ifcs { 378 s, ok := ifc.(string) 379 if !ok { 380 return nil, fmt.Errorf("unexpected type passed as a tag: %T", ifc) 381 } 382 tags[n] = s 383 } 384 return &tags, nil 385 } 386 387 var mbSuffixes = map[string]float64{ 388 "M": 1, 389 "G": 1024, 390 "T": 1024 * 1024, 391 "P": 1024 * 1024 * 1024, 392 }