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 }