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