~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/provider/maas/constraints.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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.getSupportedArchitectures()
 
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
}