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