github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/constraints/constraints.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package constraints
     5  
     6  import (
     7  	"fmt"
     8  	"math"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/errgo/errgo"
    13  
    14  	"launchpad.net/juju-core/instance"
    15  )
    16  
    17  // Value describes a user's requirements of the hardware on which units
    18  // of a service will run. Constraints are used to choose an existing machine
    19  // onto which a unit will be deployed, or to provision a new machine if no
    20  // existing one satisfies the requirements.
    21  type Value struct {
    22  
    23  	// Arch, if not nil or empty, indicates that a machine must run the named
    24  	// architecture.
    25  	Arch *string `json:"arch,omitempty" yaml:"arch,omitempty"`
    26  
    27  	// Container, if not nil, indicates that a machine must be the specified container type.
    28  	Container *instance.ContainerType `json:"container,omitempty" yaml:"container,omitempty"`
    29  
    30  	// CpuCores, if not nil, indicates that a machine must have at least that
    31  	// number of effective cores available.
    32  	CpuCores *uint64 `json:"cpu-cores,omitempty" yaml:"cpu-cores,omitempty"`
    33  
    34  	// CpuPower, if not nil, indicates that a machine must have at least that
    35  	// amount of CPU power available, where 100 CpuPower is considered to be
    36  	// equivalent to 1 Amazon ECU (or, roughly, a single 2007-era Xeon).
    37  	CpuPower *uint64 `json:"cpu-power,omitempty" yaml:"cpu-power,omitempty"`
    38  
    39  	// Mem, if not nil, indicates that a machine must have at least that many
    40  	// megabytes of RAM.
    41  	Mem *uint64 `json:"mem,omitempty" yaml:"mem,omitempty"`
    42  
    43  	// RootDisk, if not nil, indicates that a machine must have at least
    44  	// that many megabytes of disk space available in the root disk. In
    45  	// providers where the root disk is configurable at instance startup
    46  	// time, an instance with the specified amount of disk space in the OS
    47  	// disk might be requested.
    48  	RootDisk *uint64 `json:"root-disk,omitempty" yaml:"root-disk,omitempty"`
    49  
    50  	// Tags, if not nil, indicates tags that the machine must have applied to it.
    51  	// An empty list is treated the same as a nil (unspecified) list, except an
    52  	// empty list will override any default tags, where a nil list will not.
    53  	Tags *[]string `json:"tags,omitempty" yaml:"tags,omitempty"`
    54  }
    55  
    56  // IsEmpty returns if the given constraints value has no constraints set
    57  func IsEmpty(v *Value) bool {
    58  	return v == nil ||
    59  		v.Arch == nil &&
    60  			v.Container == nil &&
    61  			v.CpuCores == nil &&
    62  			v.CpuPower == nil &&
    63  			v.Mem == nil &&
    64  			v.RootDisk == nil &&
    65  			v.Tags == nil
    66  }
    67  
    68  // String expresses a constraints.Value in the language in which it was specified.
    69  func (v Value) String() string {
    70  	var strs []string
    71  	if v.Arch != nil {
    72  		strs = append(strs, "arch="+*v.Arch)
    73  	}
    74  	if v.Container != nil {
    75  		strs = append(strs, "container="+string(*v.Container))
    76  	}
    77  	if v.CpuCores != nil {
    78  		strs = append(strs, "cpu-cores="+uintStr(*v.CpuCores))
    79  	}
    80  	if v.CpuPower != nil {
    81  		strs = append(strs, "cpu-power="+uintStr(*v.CpuPower))
    82  	}
    83  	if v.Mem != nil {
    84  		s := uintStr(*v.Mem)
    85  		if s != "" {
    86  			s += "M"
    87  		}
    88  		strs = append(strs, "mem="+s)
    89  	}
    90  	if v.RootDisk != nil {
    91  		s := uintStr(*v.RootDisk)
    92  		if s != "" {
    93  			s += "M"
    94  		}
    95  		strs = append(strs, "root-disk="+s)
    96  	}
    97  	if v.Tags != nil {
    98  		s := strings.Join(*v.Tags, ",")
    99  		strs = append(strs, "tags="+s)
   100  	}
   101  	return strings.Join(strs, " ")
   102  }
   103  
   104  // WithFallbacks returns a copy of v with nil values taken from v0.
   105  func (v Value) WithFallbacks(v0 Value) Value {
   106  	v1 := v0
   107  	if v.Arch != nil {
   108  		v1.Arch = v.Arch
   109  	}
   110  	if v.Container != nil {
   111  		v1.Container = v.Container
   112  	}
   113  	if v.CpuCores != nil {
   114  		v1.CpuCores = v.CpuCores
   115  	}
   116  	if v.CpuPower != nil {
   117  		v1.CpuPower = v.CpuPower
   118  	}
   119  	if v.Mem != nil {
   120  		v1.Mem = v.Mem
   121  	}
   122  	if v.RootDisk != nil {
   123  		v1.RootDisk = v.RootDisk
   124  	}
   125  	if v.Tags != nil {
   126  		v1.Tags = v.Tags
   127  	}
   128  	return v1
   129  }
   130  
   131  func uintStr(i uint64) string {
   132  	if i == 0 {
   133  		return ""
   134  	}
   135  	return fmt.Sprintf("%d", i)
   136  }
   137  
   138  // Parse constructs a constraints.Value from the supplied arguments,
   139  // each of which must contain only spaces and name=value pairs. If any
   140  // name is specified more than once, an error is returned.
   141  func Parse(args ...string) (Value, error) {
   142  	cons := Value{}
   143  	for _, arg := range args {
   144  		raws := strings.Split(strings.TrimSpace(arg), " ")
   145  		for _, raw := range raws {
   146  			if raw == "" {
   147  				continue
   148  			}
   149  			if err := cons.setRaw(raw); err != nil {
   150  				return Value{}, err
   151  			}
   152  		}
   153  	}
   154  	return cons, nil
   155  }
   156  
   157  // MustParse constructs a constraints.Value from the supplied arguments,
   158  // as Parse, but panics on failure.
   159  func MustParse(args ...string) Value {
   160  	v, err := Parse(args...)
   161  	if err != nil {
   162  		panic(err)
   163  	}
   164  	return v
   165  }
   166  
   167  // Constraints implements gnuflag.Value for a Constraints.
   168  type ConstraintsValue struct {
   169  	Target *Value
   170  }
   171  
   172  func (v ConstraintsValue) Set(s string) error {
   173  	cons, err := Parse(s)
   174  	if err != nil {
   175  		return err
   176  	}
   177  	*v.Target = cons
   178  	return nil
   179  }
   180  
   181  func (v ConstraintsValue) String() string {
   182  	return v.Target.String()
   183  }
   184  
   185  // setRaw interprets a name=value string and sets the supplied value.
   186  func (v *Value) setRaw(raw string) error {
   187  	eq := strings.Index(raw, "=")
   188  	if eq <= 0 {
   189  		return fmt.Errorf("malformed constraint %q", raw)
   190  	}
   191  	name, str := raw[:eq], raw[eq+1:]
   192  	var err error
   193  	switch name {
   194  	case "arch":
   195  		err = v.setArch(str)
   196  	case "container":
   197  		err = v.setContainer(str)
   198  	case "cpu-cores":
   199  		err = v.setCpuCores(str)
   200  	case "cpu-power":
   201  		err = v.setCpuPower(str)
   202  	case "mem":
   203  		err = v.setMem(str)
   204  	case "root-disk":
   205  		err = v.setRootDisk(str)
   206  	case "tags":
   207  		err = v.setTags(str)
   208  	default:
   209  		return fmt.Errorf("unknown constraint %q", name)
   210  	}
   211  	if err != nil {
   212  		return errgo.Annotatef(err, "bad %q constraint", name)
   213  	}
   214  	return nil
   215  }
   216  
   217  // SetYAML is required to unmarshall a constraints.Value object
   218  // to ensure the container attribute is correctly handled when it is empty.
   219  // Because ContainerType is an alias for string, Go's reflect logic used in the
   220  // YAML decode determines that *string and *ContainerType are not assignable so
   221  // the container value of "" in the YAML is ignored.
   222  func (v *Value) SetYAML(tag string, value interface{}) bool {
   223  	values, ok := value.(map[interface{}]interface{})
   224  	if !ok {
   225  		return false
   226  	}
   227  	for k, val := range values {
   228  		vstr := fmt.Sprintf("%v", val)
   229  		var err error
   230  		switch k {
   231  		case "arch":
   232  			v.Arch = &vstr
   233  		case "container":
   234  			ctype := instance.ContainerType(vstr)
   235  			v.Container = &ctype
   236  		case "cpu-cores":
   237  			v.CpuCores, err = parseUint64(vstr)
   238  		case "cpu-power":
   239  			v.CpuPower, err = parseUint64(vstr)
   240  		case "mem":
   241  			v.Mem, err = parseUint64(vstr)
   242  		case "root-disk":
   243  			v.RootDisk, err = parseUint64(vstr)
   244  		case "tags":
   245  			v.Tags, err = parseYamlTags(val)
   246  		default:
   247  			return false
   248  		}
   249  		if err != nil {
   250  			return false
   251  		}
   252  	}
   253  	return true
   254  }
   255  
   256  func (v *Value) setContainer(str string) error {
   257  	if v.Container != nil {
   258  		return fmt.Errorf("already set")
   259  	}
   260  	if str == "" {
   261  		ctype := instance.ContainerType("")
   262  		v.Container = &ctype
   263  	} else {
   264  		ctype, err := instance.ParseContainerTypeOrNone(str)
   265  		if err != nil {
   266  			return err
   267  		}
   268  		v.Container = &ctype
   269  	}
   270  	return nil
   271  }
   272  
   273  // HasContainer returns true if the constraints.Value specifies a container.
   274  func (v *Value) HasContainer() bool {
   275  	return v.Container != nil && *v.Container != "" && *v.Container != instance.NONE
   276  }
   277  
   278  func (v *Value) setArch(str string) error {
   279  	if v.Arch != nil {
   280  		return fmt.Errorf("already set")
   281  	}
   282  	switch str {
   283  	case "":
   284  	case "amd64", "i386", "arm", "arm64", "ppc64":
   285  	default:
   286  		return fmt.Errorf("%q not recognized", str)
   287  	}
   288  	v.Arch = &str
   289  	return nil
   290  }
   291  
   292  func (v *Value) setCpuCores(str string) (err error) {
   293  	if v.CpuCores != nil {
   294  		return fmt.Errorf("already set")
   295  	}
   296  	v.CpuCores, err = parseUint64(str)
   297  	return
   298  }
   299  
   300  func (v *Value) setCpuPower(str string) (err error) {
   301  	if v.CpuPower != nil {
   302  		return fmt.Errorf("already set")
   303  	}
   304  	v.CpuPower, err = parseUint64(str)
   305  	return
   306  }
   307  
   308  func (v *Value) setMem(str string) (err error) {
   309  	if v.Mem != nil {
   310  		return fmt.Errorf("already set")
   311  	}
   312  	v.Mem, err = parseSize(str)
   313  	return
   314  }
   315  
   316  func (v *Value) setRootDisk(str string) (err error) {
   317  	if v.RootDisk != nil {
   318  		return fmt.Errorf("already set")
   319  	}
   320  	v.RootDisk, err = parseSize(str)
   321  	return
   322  }
   323  
   324  func (v *Value) setTags(str string) error {
   325  	if v.Tags != nil {
   326  		return fmt.Errorf("already set")
   327  	}
   328  	v.Tags = parseTags(str)
   329  	return nil
   330  }
   331  
   332  func parseUint64(str string) (*uint64, error) {
   333  	var value uint64
   334  	if str != "" {
   335  		if val, err := strconv.ParseUint(str, 10, 64); err != nil {
   336  			return nil, fmt.Errorf("must be a non-negative integer")
   337  		} else {
   338  			value = uint64(val)
   339  		}
   340  	}
   341  	return &value, nil
   342  }
   343  
   344  func parseSize(str string) (*uint64, error) {
   345  	var value uint64
   346  	if str != "" {
   347  		mult := 1.0
   348  		if m, ok := mbSuffixes[str[len(str)-1:]]; ok {
   349  			str = str[:len(str)-1]
   350  			mult = m
   351  		}
   352  		val, err := strconv.ParseFloat(str, 64)
   353  		if err != nil || val < 0 {
   354  			return nil, fmt.Errorf("must be a non-negative float with optional M/G/T/P suffix")
   355  		}
   356  		val *= mult
   357  		value = uint64(math.Ceil(val))
   358  	}
   359  	return &value, nil
   360  }
   361  
   362  // parseTags returns the tags in the value s.  We expect the tags to be comma delimited strings.
   363  func parseTags(s string) *[]string {
   364  	if s == "" {
   365  		return &[]string{}
   366  	}
   367  	t := strings.Split(s, ",")
   368  	return &t
   369  }
   370  
   371  func parseYamlTags(val interface{}) (*[]string, error) {
   372  	ifcs, ok := val.([]interface{})
   373  	if !ok {
   374  		return nil, fmt.Errorf("unexpected type passed to tags: %T", val)
   375  	}
   376  	tags := make([]string, len(ifcs))
   377  	for n, ifc := range ifcs {
   378  		s, ok := ifc.(string)
   379  		if !ok {
   380  			return nil, fmt.Errorf("unexpected type passed as a tag: %T", ifc)
   381  		}
   382  		tags[n] = s
   383  	}
   384  	return &tags, nil
   385  }
   386  
   387  var mbSuffixes = map[string]float64{
   388  	"M": 1,
   389  	"G": 1024,
   390  	"T": 1024 * 1024,
   391  	"P": 1024 * 1024 * 1024,
   392  }