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  }