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  }