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