github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/constraints/constraints.go (about) 1 // Copyright 2013, 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package constraints 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "math" 10 "strconv" 11 "strings" 12 13 "github.com/juju/errors" 14 "github.com/juju/utils/arch" 15 "gopkg.in/juju/names.v2" 16 17 "github.com/juju/juju/instance" 18 ) 19 20 // The following constants list the supported constraint attribute names, as defined 21 // by the fields in the Value struct. 22 const ( 23 Arch = "arch" 24 Container = "container" 25 // cpuCores is an alias for Cores. 26 cpuCores = "cpu-cores" 27 Cores = "cores" 28 CpuPower = "cpu-power" 29 Mem = "mem" 30 RootDisk = "root-disk" 31 Tags = "tags" 32 InstanceType = "instance-type" 33 Spaces = "spaces" 34 VirtType = "virt-type" 35 ) 36 37 // Value describes a user's requirements of the hardware on which units 38 // of a service will run. Constraints are used to choose an existing machine 39 // onto which a unit will be deployed, or to provision a new machine if no 40 // existing one satisfies the requirements. 41 type Value struct { 42 43 // Arch, if not nil or empty, indicates that a machine must run the named 44 // architecture. 45 Arch *string `json:"arch,omitempty" yaml:"arch,omitempty"` 46 47 // Container, if not nil, indicates that a machine must be the specified container type. 48 Container *instance.ContainerType `json:"container,omitempty" yaml:"container,omitempty"` 49 50 // CpuCores, if not nil, indicates that a machine must have at least that 51 // number of effective cores available. 52 CpuCores *uint64 `json:"cores,omitempty" yaml:"cores,omitempty"` 53 54 // CpuPower, if not nil, indicates that a machine must have at least that 55 // amount of CPU power available, where 100 CpuPower is considered to be 56 // equivalent to 1 Amazon ECU (or, roughly, a single 2007-era Xeon). 57 CpuPower *uint64 `json:"cpu-power,omitempty" yaml:"cpu-power,omitempty"` 58 59 // Mem, if not nil, indicates that a machine must have at least that many 60 // megabytes of RAM. 61 Mem *uint64 `json:"mem,omitempty" yaml:"mem,omitempty"` 62 63 // RootDisk, if not nil, indicates that a machine must have at least 64 // that many megabytes of disk space available in the root disk. In 65 // providers where the root disk is configurable at instance startup 66 // time, an instance with the specified amount of disk space in the OS 67 // disk might be requested. 68 RootDisk *uint64 `json:"root-disk,omitempty" yaml:"root-disk,omitempty"` 69 70 // Tags, if not nil, indicates tags that the machine must have applied to it. 71 // An empty list is treated the same as a nil (unspecified) list, except an 72 // empty list will override any default tags, where a nil list will not. 73 Tags *[]string `json:"tags,omitempty" yaml:"tags,omitempty"` 74 75 // InstanceType, if not nil, indicates that the specified cloud instance type 76 // be used. Only valid for clouds which support instance types. 77 InstanceType *string `json:"instance-type,omitempty" yaml:"instance-type,omitempty"` 78 79 // Spaces, if not nil, holds a list of juju network spaces that 80 // should be available (or not) on the machine. Positive and 81 // negative values are accepted, and the difference is the latter 82 // have a "^" prefix to the name. 83 Spaces *[]string `json:"spaces,omitempty" yaml:"spaces,omitempty"` 84 85 // VirtType, if not nil or empty, indicates that a machine must run the named 86 // virtual type. Only valid for clouds with multi-hypervisor support. 87 VirtType *string `json:"virt-type,omitempty" yaml:"virt-type,omitempty"` 88 } 89 90 var rawAliases = map[string]string{ 91 cpuCores: Cores, 92 } 93 94 // resolveAlias returns the canonical representation of the given key, if it'a 95 // an alias listed in aliases, otherwise it returns the original key. 96 func resolveAlias(key string) string { 97 if canonical, ok := rawAliases[key]; ok { 98 return canonical 99 } 100 return key 101 } 102 103 // IsEmpty returns if the given constraints value has no constraints set 104 func IsEmpty(v *Value) bool { 105 return v.String() == "" 106 } 107 108 // HasArch returns true if the constraints.Value specifies an architecture. 109 func (v *Value) HasArch() bool { 110 return v.Arch != nil && *v.Arch != "" 111 } 112 113 // HasMem returns true if the constraints.Value specifies a minimum amount 114 // of memory. 115 func (v *Value) HasMem() bool { 116 return v.Mem != nil && *v.Mem > 0 117 } 118 119 // HasCpuPower returns true if the constraints.Value specifies a minimum amount 120 // of CPU power. 121 func (v *Value) HasCpuPower() bool { 122 return v.CpuPower != nil && *v.CpuPower > 0 123 } 124 125 // HasCpuCores returns true if the constraints.Value specifies a minimum number 126 // of CPU cores. 127 func (v *Value) HasCpuCores() bool { 128 return v.CpuCores != nil && *v.CpuCores > 0 129 } 130 131 // HasInstanceType returns true if the constraints.Value specifies an instance type. 132 func (v *Value) HasInstanceType() bool { 133 return v.InstanceType != nil && *v.InstanceType != "" 134 } 135 136 // extractItems returns the list of entries in the given field which 137 // are either positive (included) or negative (!included; with prefix 138 // "^"). 139 func (v *Value) extractItems(field []string, included bool) []string { 140 var items []string 141 for _, name := range field { 142 prefixed := strings.HasPrefix(name, "^") 143 if prefixed && !included { 144 // has prefix and we want negatives. 145 items = append(items, strings.TrimPrefix(name, "^")) 146 } else if !prefixed && included { 147 // no prefix and we want positives. 148 items = append(items, name) 149 } 150 } 151 return items 152 } 153 154 // IncludeSpaces returns a list of spaces to include when starting a 155 // machine, if specified. 156 func (v *Value) IncludeSpaces() []string { 157 if v.Spaces == nil { 158 return nil 159 } 160 return v.extractItems(*v.Spaces, true) 161 } 162 163 // ExcludeSpaces returns a list of spaces to exclude when starting a 164 // machine, if specified. They are given in the spaces constraint with 165 // a "^" prefix to the name, which is stripped before returning. 166 func (v *Value) ExcludeSpaces() []string { 167 if v.Spaces == nil { 168 return nil 169 } 170 return v.extractItems(*v.Spaces, false) 171 } 172 173 // HaveSpaces returns whether any spaces constraints were specified. 174 func (v *Value) HaveSpaces() bool { 175 return v.Spaces != nil && len(*v.Spaces) > 0 176 } 177 178 // HasVirtType returns true if the constraints.Value specifies an virtual type. 179 func (v *Value) HasVirtType() bool { 180 return v.VirtType != nil && *v.VirtType != "" 181 } 182 183 // String expresses a constraints.Value in the language in which it was specified. 184 func (v Value) String() string { 185 var strs []string 186 if v.Arch != nil { 187 strs = append(strs, "arch="+*v.Arch) 188 } 189 if v.Container != nil { 190 strs = append(strs, "container="+string(*v.Container)) 191 } 192 if v.CpuCores != nil { 193 strs = append(strs, "cores="+uintStr(*v.CpuCores)) 194 } 195 if v.CpuPower != nil { 196 strs = append(strs, "cpu-power="+uintStr(*v.CpuPower)) 197 } 198 if v.InstanceType != nil { 199 strs = append(strs, "instance-type="+string(*v.InstanceType)) 200 } 201 if v.Mem != nil { 202 s := uintStr(*v.Mem) 203 if s != "" { 204 s += "M" 205 } 206 strs = append(strs, "mem="+s) 207 } 208 if v.RootDisk != nil { 209 s := uintStr(*v.RootDisk) 210 if s != "" { 211 s += "M" 212 } 213 strs = append(strs, "root-disk="+s) 214 } 215 if v.Tags != nil { 216 s := strings.Join(*v.Tags, ",") 217 strs = append(strs, "tags="+s) 218 } 219 if v.Spaces != nil { 220 s := strings.Join(*v.Spaces, ",") 221 strs = append(strs, "spaces="+s) 222 } 223 if v.VirtType != nil { 224 strs = append(strs, "virt-type="+string(*v.VirtType)) 225 } 226 return strings.Join(strs, " ") 227 } 228 229 // GoString allows printing a constraints.Value nicely with the fmt 230 // package, especially when nested inside other types. 231 func (v Value) GoString() string { 232 var values []string 233 if v.Arch != nil { 234 values = append(values, fmt.Sprintf("Arch: %q", *v.Arch)) 235 } 236 if v.CpuCores != nil { 237 values = append(values, fmt.Sprintf("Cores: %v", *v.CpuCores)) 238 } 239 if v.CpuPower != nil { 240 values = append(values, fmt.Sprintf("CpuPower: %v", *v.CpuPower)) 241 } 242 if v.Mem != nil { 243 values = append(values, fmt.Sprintf("Mem: %v", *v.Mem)) 244 } 245 if v.RootDisk != nil { 246 values = append(values, fmt.Sprintf("RootDisk: %v", *v.RootDisk)) 247 } 248 if v.InstanceType != nil { 249 values = append(values, fmt.Sprintf("InstanceType: %q", *v.InstanceType)) 250 } 251 if v.Container != nil { 252 values = append(values, fmt.Sprintf("Container: %q", *v.Container)) 253 } 254 if v.Tags != nil && *v.Tags != nil { 255 values = append(values, fmt.Sprintf("Tags: %q", *v.Tags)) 256 } else if v.Tags != nil { 257 values = append(values, "Tags: (*[]string)(nil)") 258 } 259 if v.Spaces != nil && *v.Spaces != nil { 260 values = append(values, fmt.Sprintf("Spaces: %q", *v.Spaces)) 261 } else if v.Spaces != nil { 262 values = append(values, "Spaces: (*[]string)(nil)") 263 } 264 if v.VirtType != nil { 265 values = append(values, fmt.Sprintf("VirtType: %q", *v.VirtType)) 266 } 267 return fmt.Sprintf("{%s}", strings.Join(values, ", ")) 268 } 269 270 func uintStr(i uint64) string { 271 if i == 0 { 272 return "" 273 } 274 return fmt.Sprintf("%d", i) 275 } 276 277 // Parse constructs a constraints.Value from the supplied arguments, 278 // each of which must contain only spaces and name=value pairs. If any 279 // name is specified more than once, an error is returned. 280 func Parse(args ...string) (Value, error) { 281 v, _, err := ParseWithAliases(args...) 282 return v, err 283 } 284 285 // ParseWithAliases constructs a constraints.Value from the supplied arguments, each 286 // of which must contain only spaces and name=value pairs. If any name is 287 // specified more than once, an error is returned. The aliases map returned 288 // contains a map of aliases used, and their canonical values. 289 func ParseWithAliases(args ...string) (cons Value, aliases map[string]string, err error) { 290 aliases = make(map[string]string) 291 for _, arg := range args { 292 raws := strings.Split(strings.TrimSpace(arg), " ") 293 for _, raw := range raws { 294 if raw == "" { 295 continue 296 } 297 name, val, err := splitRaw(raw) 298 if err != nil { 299 return Value{}, nil, errors.Trace(err) 300 } 301 if canonical, ok := rawAliases[name]; ok { 302 aliases[name] = canonical 303 name = canonical 304 } 305 if err := cons.setRaw(name, val); err != nil { 306 return Value{}, aliases, errors.Trace(err) 307 } 308 } 309 } 310 return cons, aliases, nil 311 } 312 313 // Merge returns the effective constraints after merging any given 314 // existing values. 315 func Merge(values ...Value) (Value, error) { 316 var args []string 317 for _, value := range values { 318 args = append(args, value.String()) 319 } 320 return Parse(args...) 321 } 322 323 // MustParse constructs a constraints.Value from the supplied arguments, 324 // as Parse, but panics on failure. 325 func MustParse(args ...string) Value { 326 v, err := Parse(args...) 327 if err != nil { 328 panic(err) 329 } 330 return v 331 } 332 333 // Constraints implements gnuflag.Value for a Constraints. 334 type ConstraintsValue struct { 335 Target *Value 336 } 337 338 func (v ConstraintsValue) Set(s string) error { 339 cons, err := Parse(s) 340 if err != nil { 341 return err 342 } 343 *v.Target = cons 344 return nil 345 } 346 347 func (v ConstraintsValue) String() string { 348 return v.Target.String() 349 } 350 351 // attributesWithValues returns the non-zero attribute tags and their values from the constraint. 352 func (v *Value) attributesWithValues() map[string]interface{} { 353 // These can never fail, so we ignore the error for the sake of keeping our 354 // API clean. I'm sorry (but not that sorry). 355 b, _ := json.Marshal(v) 356 result := map[string]interface{}{} 357 _ = json.Unmarshal(b, &result) 358 return result 359 } 360 361 func fromAttributes(attr map[string]interface{}) Value { 362 b, _ := json.Marshal(attr) 363 var result Value 364 _ = json.Unmarshal(b, &result) 365 return result 366 } 367 368 // hasAny returns any attrTags for which the constraint has a non-nil value. 369 func (v *Value) hasAny(attrTags ...string) []string { 370 attributes := v.attributesWithValues() 371 var result []string 372 for _, tag := range attrTags { 373 _, ok := attributes[resolveAlias(tag)] 374 if ok { 375 result = append(result, tag) 376 } 377 } 378 return result 379 } 380 381 // without returns a copy of the constraint without values for 382 // the specified attributes. 383 func (v *Value) without(attrTags ...string) Value { 384 attributes := v.attributesWithValues() 385 for _, tag := range attrTags { 386 delete(attributes, resolveAlias(tag)) 387 } 388 return fromAttributes(attributes) 389 } 390 391 func splitRaw(s string) (name, val string, err error) { 392 eq := strings.Index(s, "=") 393 if eq <= 0 { 394 return "", "", errors.Errorf("malformed constraint %q", s) 395 } 396 return s[:eq], s[eq+1:], nil 397 } 398 399 // setRaw interprets a name=value string and sets the supplied value. 400 func (v *Value) setRaw(name, str string) error { 401 var err error 402 switch resolveAlias(name) { 403 case Arch: 404 err = v.setArch(str) 405 case Container: 406 err = v.setContainer(str) 407 case Cores: 408 err = v.setCpuCores(str) 409 case CpuPower: 410 err = v.setCpuPower(str) 411 case Mem: 412 err = v.setMem(str) 413 case RootDisk: 414 err = v.setRootDisk(str) 415 case Tags: 416 err = v.setTags(str) 417 case InstanceType: 418 err = v.setInstanceType(str) 419 case Spaces: 420 err = v.setSpaces(str) 421 case VirtType: 422 err = v.setVirtType(str) 423 default: 424 return errors.Errorf("unknown constraint %q", name) 425 } 426 if err != nil { 427 return errors.Annotatef(err, "bad %q constraint", name) 428 } 429 return nil 430 } 431 432 // UnmarshalYAML is required to unmarshal a constraints.Value object 433 // to ensure the container attribute is correctly handled when it is empty. 434 // Because ContainerType is an alias for string, Go's reflect logic used in the 435 // YAML decode determines that *string and *ContainerType are not assignable so 436 // the container value of "" in the YAML is ignored. 437 func (v *Value) UnmarshalYAML(unmarshal func(interface{}) error) error { 438 values := map[interface{}]interface{}{} 439 err := unmarshal(&values) 440 if err != nil { 441 return errors.Trace(err) 442 } 443 canonicals := map[string]string{} 444 for k, val := range values { 445 vstr := fmt.Sprintf("%v", val) 446 key, ok := k.(string) 447 if !ok { 448 return errors.Errorf("unexpected non-string key: %#v", k) 449 } 450 canonical := resolveAlias(key) 451 if v, ok := canonicals[canonical]; ok { 452 // duplicate entry 453 return errors.Errorf("constraint %q duplicates constraint %q", key, v) 454 } 455 canonicals[canonical] = key 456 switch canonical { 457 case Arch: 458 v.Arch = &vstr 459 case Container: 460 ctype := instance.ContainerType(vstr) 461 v.Container = &ctype 462 case InstanceType: 463 v.InstanceType = &vstr 464 case Cores: 465 v.CpuCores, err = parseUint64(vstr) 466 case CpuPower: 467 v.CpuPower, err = parseUint64(vstr) 468 case Mem: 469 v.Mem, err = parseUint64(vstr) 470 case RootDisk: 471 v.RootDisk, err = parseUint64(vstr) 472 case Tags: 473 v.Tags, err = parseYamlStrings("tags", val) 474 case Spaces: 475 var spaces *[]string 476 spaces, err = parseYamlStrings("spaces", val) 477 if err != nil { 478 return errors.Trace(err) 479 } 480 err = v.validateSpaces(spaces) 481 if err == nil { 482 v.Spaces = spaces 483 } 484 case VirtType: 485 v.VirtType = &vstr 486 default: 487 return errors.Errorf("unknown constraint value: %v", k) 488 } 489 if err != nil { 490 return errors.Trace(err) 491 } 492 } 493 return nil 494 } 495 496 func (v *Value) setContainer(str string) error { 497 if v.Container != nil { 498 return errors.Errorf("already set") 499 } 500 if str == "" { 501 ctype := instance.ContainerType("") 502 v.Container = &ctype 503 } else { 504 ctype, err := instance.ParseContainerTypeOrNone(str) 505 if err != nil { 506 return err 507 } 508 v.Container = &ctype 509 } 510 return nil 511 } 512 513 // HasContainer returns true if the constraints.Value specifies a container. 514 func (v *Value) HasContainer() bool { 515 return v.Container != nil && *v.Container != "" && *v.Container != instance.NONE 516 } 517 518 func (v *Value) setArch(str string) error { 519 if v.Arch != nil { 520 return errors.Errorf("already set") 521 } 522 if str != "" && !arch.IsSupportedArch(str) { 523 return errors.Errorf("%q not recognized", str) 524 } 525 v.Arch = &str 526 return nil 527 } 528 529 func (v *Value) setCpuCores(str string) (err error) { 530 if v.CpuCores != nil { 531 return errors.Errorf("already set") 532 } 533 v.CpuCores, err = parseUint64(str) 534 return 535 } 536 537 func (v *Value) setCpuPower(str string) (err error) { 538 if v.CpuPower != nil { 539 return errors.Errorf("already set") 540 } 541 v.CpuPower, err = parseUint64(str) 542 return 543 } 544 545 func (v *Value) setInstanceType(str string) error { 546 if v.InstanceType != nil { 547 return errors.Errorf("already set") 548 } 549 v.InstanceType = &str 550 return nil 551 } 552 553 func (v *Value) setMem(str string) (err error) { 554 if v.Mem != nil { 555 return errors.Errorf("already set") 556 } 557 v.Mem, err = parseSize(str) 558 return 559 } 560 561 func (v *Value) setRootDisk(str string) (err error) { 562 if v.RootDisk != nil { 563 return errors.Errorf("already set") 564 } 565 v.RootDisk, err = parseSize(str) 566 return 567 } 568 569 func (v *Value) setTags(str string) error { 570 if v.Tags != nil { 571 return errors.Errorf("already set") 572 } 573 v.Tags = parseCommaDelimited(str) 574 return nil 575 } 576 577 func (v *Value) setSpaces(str string) error { 578 if v.Spaces != nil { 579 return errors.Errorf("already set") 580 } 581 spaces := parseCommaDelimited(str) 582 if err := v.validateSpaces(spaces); err != nil { 583 return err 584 } 585 v.Spaces = spaces 586 return nil 587 } 588 589 func (v *Value) validateSpaces(spaces *[]string) error { 590 if spaces == nil { 591 return nil 592 } 593 for _, name := range *spaces { 594 space := strings.TrimPrefix(name, "^") 595 if !names.IsValidSpace(space) { 596 return errors.Errorf("%q is not a valid space name", space) 597 } 598 } 599 return nil 600 } 601 602 func (v *Value) setVirtType(str string) error { 603 if v.VirtType != nil { 604 return errors.Errorf("already set") 605 } 606 v.VirtType = &str 607 return nil 608 } 609 610 func parseUint64(str string) (*uint64, error) { 611 var value uint64 612 if str != "" { 613 if val, err := strconv.ParseUint(str, 10, 64); err != nil { 614 return nil, errors.Errorf("must be a non-negative integer") 615 } else { 616 value = uint64(val) 617 } 618 } 619 return &value, nil 620 } 621 622 func parseSize(str string) (*uint64, error) { 623 var value uint64 624 if str != "" { 625 mult := 1.0 626 if m, ok := mbSuffixes[str[len(str)-1:]]; ok { 627 str = str[:len(str)-1] 628 mult = m 629 } 630 val, err := strconv.ParseFloat(str, 64) 631 if err != nil || val < 0 { 632 return nil, errors.Errorf("must be a non-negative float with optional M/G/T/P suffix") 633 } 634 val *= mult 635 value = uint64(math.Ceil(val)) 636 } 637 return &value, nil 638 } 639 640 // parseCommaDelimited returns the items in the value s. We expect the 641 // items to be comma delimited strings. 642 func parseCommaDelimited(s string) *[]string { 643 if s == "" { 644 return &[]string{} 645 } 646 t := strings.Split(s, ",") 647 return &t 648 } 649 650 func parseYamlStrings(entityName string, val interface{}) (*[]string, error) { 651 ifcs, ok := val.([]interface{}) 652 if !ok { 653 return nil, errors.Errorf("unexpected type passed to %s: %T", entityName, val) 654 } 655 items := make([]string, len(ifcs)) 656 for n, ifc := range ifcs { 657 s, ok := ifc.(string) 658 if !ok { 659 return nil, errors.Errorf("unexpected type passed as in %s: %T", entityName, ifc) 660 } 661 items[n] = s 662 } 663 return &items, nil 664 } 665 666 var mbSuffixes = map[string]float64{ 667 "M": 1, 668 "G": 1024, 669 "T": 1024 * 1024, 670 "P": 1024 * 1024 * 1024, 671 }