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 }