github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/provider/maas/constraints.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package maas
     5  
     6  import (
     7  	"fmt"
     8  	"net/url"
     9  	"strings"
    10  
    11  	"github.com/juju/errors"
    12  	"github.com/juju/gomaasapi"
    13  	"github.com/juju/utils/set"
    14  
    15  	"github.com/juju/juju/constraints"
    16  	"github.com/juju/juju/network"
    17  )
    18  
    19  var unsupportedConstraints = []string{
    20  	constraints.CpuPower,
    21  	constraints.InstanceType,
    22  	constraints.VirtType,
    23  }
    24  
    25  // ConstraintsValidator is defined on the Environs interface.
    26  func (environ *maasEnviron) ConstraintsValidator() (constraints.Validator, error) {
    27  	validator := constraints.NewValidator()
    28  	validator.RegisterUnsupported(unsupportedConstraints)
    29  	supportedArches, err := environ.SupportedArchitectures()
    30  	if err != nil {
    31  		return nil, err
    32  	}
    33  	validator.RegisterVocabulary(constraints.Arch, supportedArches)
    34  	return validator, nil
    35  }
    36  
    37  // convertConstraints converts the given constraints into an url.Values object
    38  // suitable to pass to MAAS when acquiring a node. CpuPower is ignored because
    39  // it cannot be translated into something meaningful for MAAS right now.
    40  func convertConstraints(cons constraints.Value) url.Values {
    41  	params := url.Values{}
    42  	if cons.Arch != nil {
    43  		// Note: Juju and MAAS use the same architecture names.
    44  		// MAAS also accepts a subarchitecture (e.g. "highbank"
    45  		// for ARM), which defaults to "generic" if unspecified.
    46  		params.Add("arch", *cons.Arch)
    47  	}
    48  	if cons.CpuCores != nil {
    49  		params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
    50  	}
    51  	if cons.Mem != nil {
    52  		params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
    53  	}
    54  	convertTagsToParams(params, cons.Tags)
    55  	if cons.CpuPower != nil {
    56  		logger.Warningf("ignoring unsupported constraint 'cpu-power'")
    57  	}
    58  	return params
    59  }
    60  
    61  // convertConstraints2 converts the given constraints into a
    62  // gomaasapi.AllocateMachineArgs for paasing to MAAS 2.
    63  func convertConstraints2(cons constraints.Value) gomaasapi.AllocateMachineArgs {
    64  	params := gomaasapi.AllocateMachineArgs{}
    65  	if cons.Arch != nil {
    66  		params.Architecture = *cons.Arch
    67  	}
    68  	if cons.CpuCores != nil {
    69  		params.MinCPUCount = int(*cons.CpuCores)
    70  	}
    71  	if cons.Mem != nil {
    72  		params.MinMemory = int(*cons.Mem)
    73  	}
    74  	if cons.Tags != nil {
    75  		positives, negatives := parseDelimitedValues(*cons.Tags)
    76  		if len(positives) > 0 {
    77  			params.Tags = positives
    78  		}
    79  		if len(negatives) > 0 {
    80  			params.NotTags = negatives
    81  		}
    82  	}
    83  	if cons.CpuPower != nil {
    84  		logger.Warningf("ignoring unsupported constraint 'cpu-power'")
    85  	}
    86  	return params
    87  }
    88  
    89  // convertTagsToParams converts a list of positive/negative tags from
    90  // constraints into two comma-delimited lists of values, which can then be
    91  // passed to MAAS using the "tags" and "not_tags" arguments to acquire. If
    92  // either list of tags is empty, the respective argument is not added to params.
    93  func convertTagsToParams(params url.Values, tags *[]string) {
    94  	if tags == nil || len(*tags) == 0 {
    95  		return
    96  	}
    97  	positives, negatives := parseDelimitedValues(*tags)
    98  	if len(positives) > 0 {
    99  		params.Add("tags", strings.Join(positives, ","))
   100  	}
   101  	if len(negatives) > 0 {
   102  		params.Add("not_tags", strings.Join(negatives, ","))
   103  	}
   104  }
   105  
   106  // convertSpacesFromConstraints extracts spaces from constraints and converts
   107  // them to two lists of positive and negative spaces.
   108  func convertSpacesFromConstraints(spaces *[]string) ([]string, []string) {
   109  	if spaces == nil || len(*spaces) == 0 {
   110  		return nil, nil
   111  	}
   112  	return parseDelimitedValues(*spaces)
   113  }
   114  
   115  // parseDelimitedValues parses a slice of raw values coming from constraints
   116  // (Tags or Spaces). The result is split into two slices - positives and
   117  // negatives (prefixed with "^"). Empty values are ignored.
   118  func parseDelimitedValues(rawValues []string) (positives, negatives []string) {
   119  	for _, value := range rawValues {
   120  		if value == "" || value == "^" {
   121  			// Neither of these cases should happen in practise, as constraints
   122  			// are validated before setting them and empty names for spaces or
   123  			// tags are not allowed.
   124  			continue
   125  		}
   126  		if strings.HasPrefix(value, "^") {
   127  			negatives = append(negatives, strings.TrimPrefix(value, "^"))
   128  		} else {
   129  			positives = append(positives, value)
   130  		}
   131  	}
   132  	return positives, negatives
   133  }
   134  
   135  // interfaceBinding defines a requirement that a node interface must satisfy in
   136  // order for that node to get selected and started, based on deploy-time
   137  // bindings of a service.
   138  //
   139  // TODO(dimitern): Once the services have bindings defined in state, a version
   140  // of this should go to the network package (needs to be non-MAAS-specifc
   141  // first). Also, we need to transform Juju space names from constraints into
   142  // MAAS space provider IDs.
   143  type interfaceBinding struct {
   144  	Name            string
   145  	SpaceProviderId string
   146  
   147  	// add more as needed.
   148  }
   149  
   150  // numericLabelLimit is a sentinel value used in addInterfaces to limit the
   151  // number of disabmiguation inner loop iterations in case named labels clash
   152  // with numeric labels for spaces coming from constraints. It's defined here to
   153  // facilitate testing this behavior.
   154  var numericLabelLimit uint = 0xffff
   155  
   156  // addInterfaces converts a slice of interface bindings, postiveSpaces and
   157  // negativeSpaces coming from constraints to the format MAAS expects for the
   158  // "interfaces" and "not_networks" arguments to acquire node. Returns an error
   159  // satisfying errors.IsNotValid() if the bindings contains duplicates, empty
   160  // Name/SpaceProviderId, or if negative spaces clash with specified bindings.
   161  // Duplicates between specified bindings and positiveSpaces are silently
   162  // skipped.
   163  func addInterfaces(
   164  	params url.Values,
   165  	bindings []interfaceBinding,
   166  	positiveSpaces, negativeSpaces []network.SpaceInfo,
   167  ) error {
   168  	combinedBindings, negatives, err := getBindings(bindings, positiveSpaces, negativeSpaces)
   169  	if err != nil {
   170  		return errors.Trace(err)
   171  	}
   172  	if len(combinedBindings) > 0 {
   173  		combinedBindingsString := make([]string, len(combinedBindings))
   174  		for i, binding := range combinedBindings {
   175  			combinedBindingsString[i] = fmt.Sprintf("%s:space=%s", binding.Name, binding.SpaceProviderId)
   176  		}
   177  		params.Add("interfaces", strings.Join(combinedBindingsString, ";"))
   178  	}
   179  	if len(negatives) > 0 {
   180  		negativesString := make([]string, len(negatives))
   181  		for i, binding := range negatives {
   182  			negativesString[i] = fmt.Sprintf("space:%s", binding.SpaceProviderId)
   183  		}
   184  		params.Add("not_networks", strings.Join(negativesString, ","))
   185  	}
   186  	return nil
   187  }
   188  
   189  func getBindings(
   190  	bindings []interfaceBinding,
   191  	positiveSpaces, negativeSpaces []network.SpaceInfo,
   192  ) ([]interfaceBinding, []interfaceBinding, error) {
   193  	var (
   194  		index            uint
   195  		combinedBindings []interfaceBinding
   196  	)
   197  	namesSet := set.NewStrings()
   198  	spacesSet := set.NewStrings()
   199  	for _, binding := range bindings {
   200  		switch {
   201  		case binding.Name == "":
   202  			return nil, nil, errors.NewNotValid(nil, "interface bindings cannot have empty names")
   203  		case binding.SpaceProviderId == "":
   204  			return nil, nil, errors.NewNotValid(nil, fmt.Sprintf(
   205  				"invalid interface binding %q: space provider ID is required",
   206  				binding.Name,
   207  			))
   208  		case namesSet.Contains(binding.Name):
   209  			return nil, nil, errors.NewNotValid(nil, fmt.Sprintf(
   210  				"duplicated interface binding %q",
   211  				binding.Name,
   212  			))
   213  		}
   214  		namesSet.Add(binding.Name)
   215  		spacesSet.Add(binding.SpaceProviderId)
   216  
   217  		combinedBindings = append(combinedBindings, binding)
   218  	}
   219  
   220  	createLabel := func(index uint, namesSet set.Strings) (string, uint, error) {
   221  		var label string
   222  		for {
   223  			label = fmt.Sprintf("%v", index)
   224  			if !namesSet.Contains(label) {
   225  				break
   226  			}
   227  			if index > numericLabelLimit { // ...just to make sure we won't loop forever.
   228  				return "", index, errors.Errorf("too many conflicting numeric labels, giving up.")
   229  			}
   230  			index++
   231  		}
   232  		namesSet.Add(label)
   233  		return label, index, nil
   234  	}
   235  	for _, space := range positiveSpaces {
   236  		if spacesSet.Contains(string(space.ProviderId)) {
   237  			// Skip duplicates in positiveSpaces.
   238  			continue
   239  		}
   240  		spacesSet.Add(string(space.ProviderId))
   241  
   242  		var label string
   243  		var err error
   244  		label, index, err = createLabel(index, namesSet)
   245  		if err != nil {
   246  			return nil, nil, errors.Trace(err)
   247  		}
   248  		// Make sure we pick a label that doesn't clash with possible bindings.
   249  		combinedBindings = append(combinedBindings, interfaceBinding{label, string(space.ProviderId)})
   250  	}
   251  
   252  	var negatives []interfaceBinding
   253  	for _, space := range negativeSpaces {
   254  		if spacesSet.Contains(string(space.ProviderId)) {
   255  			return nil, nil, errors.NewNotValid(nil, fmt.Sprintf(
   256  				"negative space %q from constraints clashes with interface bindings",
   257  				space.Name,
   258  			))
   259  		}
   260  		var label string
   261  		var err error
   262  		label, index, err = createLabel(index, namesSet)
   263  		if err != nil {
   264  			return nil, nil, errors.Trace(err)
   265  		}
   266  		negatives = append(negatives, interfaceBinding{label, string(space.ProviderId)})
   267  	}
   268  	return combinedBindings, negatives, nil
   269  }
   270  
   271  func addInterfaces2(
   272  	params *gomaasapi.AllocateMachineArgs,
   273  	bindings []interfaceBinding,
   274  	positiveSpaces, negativeSpaces []network.SpaceInfo,
   275  ) error {
   276  	combinedBindings, negatives, err := getBindings(bindings, positiveSpaces, negativeSpaces)
   277  	if err != nil {
   278  		return errors.Trace(err)
   279  	}
   280  
   281  	if len(combinedBindings) > 0 {
   282  		interfaceSpecs := make([]gomaasapi.InterfaceSpec, len(combinedBindings))
   283  		for i, space := range combinedBindings {
   284  			interfaceSpecs[i] = gomaasapi.InterfaceSpec{space.Name, space.SpaceProviderId}
   285  		}
   286  		params.Interfaces = interfaceSpecs
   287  	}
   288  	if len(negatives) > 0 {
   289  		negativeStrings := make([]string, len(negatives))
   290  		for i, space := range negatives {
   291  			negativeStrings[i] = space.SpaceProviderId
   292  		}
   293  		params.NotSpace = negativeStrings
   294  	}
   295  	return nil
   296  }
   297  
   298  // addStorage converts volume information into url.Values object suitable to
   299  // pass to MAAS when acquiring a node.
   300  func addStorage(params url.Values, volumes []volumeInfo) {
   301  	if len(volumes) == 0 {
   302  		return
   303  	}
   304  	// Requests for specific values are passed to the acquire URL
   305  	// as a storage URL parameter of the form:
   306  	// [volume-name:]sizeinGB[tag,...]
   307  	// See http://maas.ubuntu.com/docs/api.html#nodes
   308  
   309  	// eg storage=root:0(ssd),data:20(magnetic,5400rpm),45
   310  	makeVolumeParams := func(v volumeInfo) string {
   311  		var params string
   312  		if v.name != "" {
   313  			params = v.name + ":"
   314  		}
   315  		params += fmt.Sprintf("%d", v.sizeInGB)
   316  		if len(v.tags) > 0 {
   317  			params += fmt.Sprintf("(%s)", strings.Join(v.tags, ","))
   318  		}
   319  		return params
   320  	}
   321  	var volParms []string
   322  	for _, v := range volumes {
   323  		params := makeVolumeParams(v)
   324  		volParms = append(volParms, params)
   325  	}
   326  	params.Add("storage", strings.Join(volParms, ","))
   327  }
   328  
   329  // addStorage2 adds volume information onto a gomaasapi.AllocateMachineArgs
   330  // object suitable to pass to MAAS 2 when acquiring a node.
   331  func addStorage2(params *gomaasapi.AllocateMachineArgs, volumes []volumeInfo) {
   332  	if len(volumes) == 0 {
   333  		return
   334  	}
   335  	var volParams []gomaasapi.StorageSpec
   336  	for _, v := range volumes {
   337  		volSpec := gomaasapi.StorageSpec{
   338  			Label: v.name,
   339  			Size:  int(v.sizeInGB),
   340  			Tags:  v.tags,
   341  		}
   342  		volParams = append(volParams, volSpec)
   343  	}
   344  	params.Storage = volParams
   345  }