1
// Copyright 2015 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
11
"github.com/juju/errors"
12
"github.com/juju/gomaasapi"
13
"github.com/juju/utils/set"
15
"github.com/juju/juju/constraints"
16
"github.com/juju/juju/network"
19
var unsupportedConstraints = []string{
21
constraints.InstanceType,
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.getSupportedArchitectures()
33
validator.RegisterVocabulary(constraints.Arch, supportedArches)
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{}
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)
48
if cons.CpuCores != nil {
49
params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores))
52
params.Add("mem", fmt.Sprintf("%d", *cons.Mem))
54
convertTagsToParams(params, cons.Tags)
55
if cons.CpuPower != nil {
56
logger.Warningf("ignoring unsupported constraint 'cpu-power'")
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{}
66
params.Architecture = *cons.Arch
68
if cons.CpuCores != nil {
69
params.MinCPUCount = int(*cons.CpuCores)
72
params.MinMemory = int(*cons.Mem)
75
positives, negatives := parseDelimitedValues(*cons.Tags)
76
if len(positives) > 0 {
77
params.Tags = positives
79
if len(negatives) > 0 {
80
params.NotTags = negatives
83
if cons.CpuPower != nil {
84
logger.Warningf("ignoring unsupported constraint 'cpu-power'")
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 {
97
positives, negatives := parseDelimitedValues(*tags)
98
if len(positives) > 0 {
99
params.Add("tags", strings.Join(positives, ","))
101
if len(negatives) > 0 {
102
params.Add("not_tags", strings.Join(negatives, ","))
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 {
112
return parseDelimitedValues(*spaces)
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.
126
if strings.HasPrefix(value, "^") {
127
negatives = append(negatives, strings.TrimPrefix(value, "^"))
129
positives = append(positives, value)
132
return positives, negatives
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.
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 {
145
SpaceProviderId string
147
// add more as needed.
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
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
165
bindings []interfaceBinding,
166
positiveSpaces, negativeSpaces []network.SpaceInfo,
168
combinedBindings, negatives, err := getBindings(bindings, positiveSpaces, negativeSpaces)
170
return errors.Trace(err)
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)
177
params.Add("interfaces", strings.Join(combinedBindingsString, ";"))
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)
184
params.Add("not_networks", strings.Join(negativesString, ","))
190
bindings []interfaceBinding,
191
positiveSpaces, negativeSpaces []network.SpaceInfo,
192
) ([]interfaceBinding, []interfaceBinding, error) {
195
combinedBindings []interfaceBinding
197
namesSet := set.NewStrings()
198
spacesSet := set.NewStrings()
199
for _, binding := range bindings {
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",
208
case namesSet.Contains(binding.Name):
209
return nil, nil, errors.NewNotValid(nil, fmt.Sprintf(
210
"duplicated interface binding %q",
214
namesSet.Add(binding.Name)
215
spacesSet.Add(binding.SpaceProviderId)
217
combinedBindings = append(combinedBindings, binding)
220
createLabel := func(index uint, namesSet set.Strings) (string, uint, error) {
223
label = fmt.Sprintf("%v", index)
224
if !namesSet.Contains(label) {
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.")
233
return label, index, nil
235
for _, space := range positiveSpaces {
236
if spacesSet.Contains(string(space.ProviderId)) {
237
// Skip duplicates in positiveSpaces.
240
spacesSet.Add(string(space.ProviderId))
244
label, index, err = createLabel(index, namesSet)
246
return nil, nil, errors.Trace(err)
248
// Make sure we pick a label that doesn't clash with possible bindings.
249
combinedBindings = append(combinedBindings, interfaceBinding{label, string(space.ProviderId)})
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",
262
label, index, err = createLabel(index, namesSet)
264
return nil, nil, errors.Trace(err)
266
negatives = append(negatives, interfaceBinding{label, string(space.ProviderId)})
268
return combinedBindings, negatives, nil
272
params *gomaasapi.AllocateMachineArgs,
273
bindings []interfaceBinding,
274
positiveSpaces, negativeSpaces []network.SpaceInfo,
276
combinedBindings, negatives, err := getBindings(bindings, positiveSpaces, negativeSpaces)
278
return errors.Trace(err)
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}
286
params.Interfaces = interfaceSpecs
288
if len(negatives) > 0 {
289
negativeStrings := make([]string, len(negatives))
290
for i, space := range negatives {
291
negativeStrings[i] = space.SpaceProviderId
293
params.NotSpace = negativeStrings
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 {
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
309
// eg storage=root:0(ssd),data:20(magnetic,5400rpm),45
310
makeVolumeParams := func(v volumeInfo) string {
313
params = v.name + ":"
315
params += fmt.Sprintf("%d", v.sizeInGB)
317
params += fmt.Sprintf("(%s)", strings.Join(v.tags, ","))
321
var volParms []string
322
for _, v := range volumes {
323
params := makeVolumeParams(v)
324
volParms = append(volParms, params)
326
params.Add("storage", strings.Join(volParms, ","))
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 {
335
var volParams []gomaasapi.StorageSpec
336
for _, v := range volumes {
337
volSpec := gomaasapi.StorageSpec{
339
Size: int(v.sizeInGB),
342
volParams = append(volParams, volSpec)
344
params.Storage = volParams