github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/maas/volumes.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 "strconv" 8 "strings" 9 "unicode" 10 11 "github.com/dustin/go-humanize" 12 "github.com/juju/collections/set" 13 "github.com/juju/errors" 14 "github.com/juju/gomaasapi" 15 "github.com/juju/schema" 16 "gopkg.in/juju/names.v2" 17 18 "github.com/juju/juju/core/constraints" 19 "github.com/juju/juju/provider/common" 20 "github.com/juju/juju/storage" 21 ) 22 23 const ( 24 // maasStorageProviderType is the name of the storage provider 25 // used to specify storage when acquiring MAAS nodes. 26 maasStorageProviderType = storage.ProviderType("maas") 27 28 // rootDiskLabel is the label recognised by MAAS as being for 29 // the root disk. 30 rootDiskLabel = "root" 31 32 // tagsAttribute is the name of the pool attribute used 33 // to specify tag values for requested volumes. 34 tagsAttribute = "tags" 35 ) 36 37 // StorageProviderTypes implements storage.ProviderRegistry. 38 func (*maasEnviron) StorageProviderTypes() ([]storage.ProviderType, error) { 39 return []storage.ProviderType{maasStorageProviderType}, nil 40 } 41 42 // StorageProvider implements storage.ProviderRegistry. 43 func (*maasEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 44 if t == maasStorageProviderType { 45 return maasStorageProvider{}, nil 46 } 47 return nil, errors.NotFoundf("storage provider %q", t) 48 } 49 50 // maasStorageProvider allows volumes to be specified when a node is acquired. 51 type maasStorageProvider struct{} 52 53 var storageConfigFields = schema.Fields{ 54 tagsAttribute: schema.OneOf( 55 schema.List(schema.String()), 56 schema.String(), 57 ), 58 } 59 60 var storageConfigChecker = schema.FieldMap( 61 storageConfigFields, 62 schema.Defaults{ 63 tagsAttribute: schema.Omit, 64 }, 65 ) 66 67 type storageConfig struct { 68 tags []string 69 } 70 71 func newStorageConfig(attrs map[string]interface{}) (*storageConfig, error) { 72 out, err := storageConfigChecker.Coerce(attrs, nil) 73 if err != nil { 74 return nil, errors.Annotate(err, "validating MAAS storage config") 75 } 76 coerced := out.(map[string]interface{}) 77 var tags []string 78 switch v := coerced[tagsAttribute].(type) { 79 case []string: 80 tags = v 81 case string: 82 fields := strings.Split(v, ",") 83 for _, f := range fields { 84 f = strings.TrimSpace(f) 85 if len(f) == 0 { 86 continue 87 } 88 if i := strings.IndexFunc(f, unicode.IsSpace); i >= 0 { 89 return nil, errors.Errorf("tags may not contain whitespace: %q", f) 90 } 91 tags = append(tags, f) 92 } 93 } 94 return &storageConfig{tags: tags}, nil 95 } 96 97 // ValidateConfig is defined on the Provider interface. 98 func (maasStorageProvider) ValidateConfig(cfg *storage.Config) error { 99 _, err := newStorageConfig(cfg.Attrs()) 100 return errors.Trace(err) 101 } 102 103 // Supports is defined on the Provider interface. 104 func (maasStorageProvider) Supports(k storage.StorageKind) bool { 105 return k == storage.StorageKindBlock 106 } 107 108 // Scope is defined on the Provider interface. 109 func (maasStorageProvider) Scope() storage.Scope { 110 return storage.ScopeEnviron 111 } 112 113 // Dynamic is defined on the Provider interface. 114 func (maasStorageProvider) Dynamic() bool { 115 return false 116 } 117 118 // Releasable is defined on the Provider interface. 119 func (maasStorageProvider) Releasable() bool { 120 return false 121 } 122 123 // DefaultPools is defined on the Provider interface. 124 func (maasStorageProvider) DefaultPools() []*storage.Config { 125 return nil 126 } 127 128 // VolumeSource is defined on the Provider interface. 129 func (maasStorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { 130 // Dynamic volumes not supported. 131 return nil, errors.NotSupportedf("volumes") 132 } 133 134 // FilesystemSource is defined on the Provider interface. 135 func (maasStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 136 return nil, errors.NotSupportedf("filesystems") 137 } 138 139 type volumeInfo struct { 140 name string 141 sizeInGB uint64 142 tags []string 143 } 144 145 // mibToGB converts the value in MiB to GB. 146 // Juju works in MiB, MAAS expects GB. 147 func mibToGb(m uint64) uint64 { 148 return common.MiBToGiB(m) * (humanize.GiByte / humanize.GByte) 149 } 150 151 // buildMAASVolumeParameters creates the MAAS volume information to include 152 // in a request to acquire a MAAS node, based on the supplied storage parameters. 153 func buildMAASVolumeParameters(args []storage.VolumeParams, cons constraints.Value) ([]volumeInfo, error) { 154 if len(args) == 0 && cons.RootDisk == nil { 155 return nil, nil 156 } 157 volumes := make([]volumeInfo, len(args)+1) 158 rootVolume := volumeInfo{name: rootDiskLabel} 159 if cons.RootDisk != nil { 160 rootVolume.sizeInGB = mibToGb(*cons.RootDisk) 161 } 162 volumes[0] = rootVolume 163 for i, v := range args { 164 cfg, err := newStorageConfig(v.Attributes) 165 if err != nil { 166 return nil, errors.Trace(err) 167 } 168 info := volumeInfo{ 169 name: v.Tag.Id(), 170 sizeInGB: mibToGb(v.Size), 171 tags: cfg.tags, 172 } 173 volumes[i+1] = info 174 } 175 return volumes, nil 176 } 177 178 // volumes creates the storage volumes and attachments 179 // corresponding to the volume info associated with a MAAS node. 180 func (mi *maas1Instance) volumes( 181 mTag names.MachineTag, requestedVolumes []names.VolumeTag, 182 ) ( 183 []storage.Volume, []storage.VolumeAttachment, error, 184 ) { 185 var volumes []storage.Volume 186 var attachments []storage.VolumeAttachment 187 188 deviceInfo, ok := mi.maasObject.GetMap()["physicalblockdevice_set"] 189 // Older MAAS servers don't support storage. 190 if !ok || deviceInfo.IsNil() { 191 return volumes, attachments, nil 192 } 193 194 labelsMap, ok := mi.maasObject.GetMap()["constraint_map"] 195 if !ok || labelsMap.IsNil() { 196 return nil, nil, errors.NotFoundf("constraint map field") 197 } 198 199 devices, err := deviceInfo.GetArray() 200 if err != nil { 201 return nil, nil, errors.Trace(err) 202 } 203 // deviceLabel is the volume label passed 204 // into the acquire node call as part 205 // of the storage constraints parameter. 206 deviceLabels, err := labelsMap.GetMap() 207 if err != nil { 208 return nil, nil, errors.Annotate(err, "invalid constraint map value") 209 } 210 211 // Set up a collection of volumes tags which 212 // we specifically asked for when the node was acquired. 213 validVolumes := set.NewStrings() 214 for _, v := range requestedVolumes { 215 validVolumes.Add(v.Id()) 216 } 217 218 for _, d := range devices { 219 deviceAttrs, err := d.GetMap() 220 if err != nil { 221 return nil, nil, errors.Trace(err) 222 } 223 // id in devices list is numeric 224 id, err := deviceAttrs["id"].GetFloat64() 225 if err != nil { 226 return nil, nil, errors.Annotate(err, "invalid device id") 227 } 228 // id in constraint_map field is a string 229 idKey := strconv.Itoa(int(id)) 230 231 // Device Label. 232 deviceLabelValue, ok := deviceLabels[idKey] 233 if !ok { 234 logger.Debugf("acquire maas node: missing volume label for id %q", idKey) 235 continue 236 } 237 deviceLabel, err := deviceLabelValue.GetString() 238 if err != nil { 239 return nil, nil, errors.Annotate(err, "invalid device label") 240 } 241 // We don't explicitly allow the root volume to be specified yet. 242 if deviceLabel == rootDiskLabel { 243 continue 244 } 245 // We only care about the volumes we specifically asked for. 246 if !validVolumes.Contains(deviceLabel) { 247 continue 248 } 249 250 // HardwareId and DeviceName. 251 // First try for id_path. 252 idPathPrefix := "/dev/disk/by-id/" 253 hardwareId, err := deviceAttrs["id_path"].GetString() 254 var deviceName string 255 if err == nil { 256 if !strings.HasPrefix(hardwareId, idPathPrefix) { 257 return nil, nil, errors.Errorf("invalid device id %q", hardwareId) 258 } 259 hardwareId = hardwareId[len(idPathPrefix):] 260 } else { 261 // On VMAAS, id_path not available so try for path instead. 262 deviceName, err = deviceAttrs["name"].GetString() 263 if err != nil { 264 return nil, nil, errors.Annotate(err, "invalid device name") 265 } 266 } 267 268 // Size. 269 sizeinBytes, err := deviceAttrs["size"].GetFloat64() 270 if err != nil { 271 return nil, nil, errors.Annotate(err, "invalid device size") 272 } 273 274 volumeTag := names.NewVolumeTag(deviceLabel) 275 vol := storage.Volume{ 276 volumeTag, 277 storage.VolumeInfo{ 278 VolumeId: volumeTag.String(), 279 HardwareId: hardwareId, 280 Size: uint64(sizeinBytes / humanize.MiByte), 281 Persistent: false, 282 }, 283 } 284 volumes = append(volumes, vol) 285 286 attachment := storage.VolumeAttachment{ 287 volumeTag, 288 mTag, 289 storage.VolumeAttachmentInfo{ 290 DeviceName: deviceName, 291 ReadOnly: false, 292 }, 293 } 294 attachments = append(attachments, attachment) 295 } 296 return volumes, attachments, nil 297 } 298 299 func (mi *maas2Instance) volumes( 300 mTag names.MachineTag, requestedVolumes []names.VolumeTag, 301 ) ( 302 []storage.Volume, []storage.VolumeAttachment, error, 303 ) { 304 if mi.constraintMatches.Storage == nil { 305 return nil, nil, errors.NotFoundf("constraint storage mapping") 306 } 307 308 var volumes []storage.Volume 309 var attachments []storage.VolumeAttachment 310 311 // Set up a collection of volumes tags which 312 // we specifically asked for when the node was acquired. 313 validVolumes := set.NewStrings() 314 for _, v := range requestedVolumes { 315 validVolumes.Add(v.Id()) 316 } 317 318 for label, devices := range mi.constraintMatches.Storage { 319 // We don't explicitly allow the root volume to be specified yet. 320 if label == rootDiskLabel { 321 continue 322 } 323 324 // We only care about the volumes we specifically asked for. 325 if !validVolumes.Contains(label) { 326 continue 327 } 328 329 // There should be exactly one block device per label. 330 if len(devices) == 0 { 331 continue 332 } else if len(devices) > 1 { 333 // This should never happen, as we only request one block 334 // device per label. If it does happen, we'll just report 335 // the first block device and log this warning. 336 logger.Warningf( 337 "expected 1 block device for label %s, received %d", 338 label, len(devices), 339 ) 340 } 341 342 device := devices[0] 343 volumeTag := names.NewVolumeTag(label) 344 vol := storage.Volume{ 345 volumeTag, 346 storage.VolumeInfo{ 347 VolumeId: volumeTag.String(), 348 Size: device.Size() / humanize.MiByte, 349 Persistent: false, 350 }, 351 } 352 attachment := storage.VolumeAttachment{ 353 volumeTag, 354 mTag, 355 storage.VolumeAttachmentInfo{ 356 ReadOnly: false, 357 }, 358 } 359 360 const devDiskByIdPrefix = "/dev/disk/by-id/" 361 const devPrefix = "/dev/" 362 363 if blockDev, ok := device.(gomaasapi.BlockDevice); ok { 364 // Handle a block device specifically that way the path used 365 // by Juju will always be a persistent path. 366 idPath := blockDev.IDPath() 367 if idPath == devPrefix+blockDev.Name() { 368 // On vMAAS (i.e. with virtio), the device name 369 // will be stable, and is what is used to form 370 // id_path. 371 deviceName := idPath[len(devPrefix):] 372 attachment.DeviceName = deviceName 373 } else if strings.HasPrefix(idPath, devDiskByIdPrefix) { 374 const wwnPrefix = "wwn-" 375 id := idPath[len(devDiskByIdPrefix):] 376 if strings.HasPrefix(id, wwnPrefix) { 377 vol.WWN = id[len(wwnPrefix):] 378 } else { 379 vol.HardwareId = id 380 } 381 } else { 382 // It's neither /dev/<name> nor /dev/disk/by-id/<hardware-id>, 383 // so set it as the device link and hope for 384 // the best. At worst, the path won't exist 385 // and the storage will remain pending. 386 attachment.DeviceLink = idPath 387 } 388 } else { 389 // Handle all other storage devices using the path MAAS provided. 390 // In the case of partitions the path is always stable because its 391 // based on the GUID of the partition using the dname path. 392 attachment.DeviceLink = device.Path() 393 } 394 395 volumes = append(volumes, vol) 396 attachments = append(attachments, attachment) 397 } 398 return volumes, attachments, nil 399 }