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