github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 "strings" 8 "unicode" 9 10 "github.com/dustin/go-humanize" 11 "github.com/juju/collections/set" 12 "github.com/juju/errors" 13 "github.com/juju/gomaasapi/v2" 14 "github.com/juju/names/v5" 15 "github.com/juju/schema" 16 17 "github.com/juju/juju/core/constraints" 18 "github.com/juju/juju/provider/common" 19 "github.com/juju/juju/storage" 20 ) 21 22 const ( 23 // maasStorageProviderType is the name of the storage provider 24 // used to specify storage when acquiring MAAS nodes. 25 maasStorageProviderType = storage.ProviderType("maas") 26 27 // rootDiskLabel is the label recognised by MAAS as being for 28 // the root disk. 29 rootDiskLabel = "root" 30 31 // tagsAttribute is the name of the pool attribute used 32 // to specify tag values for requested volumes. 33 tagsAttribute = "tags" 34 ) 35 36 // StorageProviderTypes implements storage.ProviderRegistry. 37 func (*maasEnviron) StorageProviderTypes() ([]storage.ProviderType, error) { 38 return []storage.ProviderType{maasStorageProviderType}, nil 39 } 40 41 // StorageProvider implements storage.ProviderRegistry. 42 func (*maasEnviron) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 43 if t == maasStorageProviderType { 44 return maasStorageProvider{}, nil 45 } 46 return nil, errors.NotFoundf("storage provider %q", t) 47 } 48 49 // maasStorageProvider allows volumes to be specified when a node is acquired. 50 type maasStorageProvider struct{} 51 52 var storageConfigFields = schema.Fields{ 53 tagsAttribute: schema.OneOf( 54 schema.List(schema.String()), 55 schema.String(), 56 ), 57 } 58 59 var storageConfigChecker = schema.FieldMap( 60 storageConfigFields, 61 schema.Defaults{ 62 tagsAttribute: schema.Omit, 63 }, 64 ) 65 66 type storageConfig struct { 67 tags []string 68 } 69 70 func newStorageConfig(attrs map[string]interface{}) (*storageConfig, error) { 71 out, err := storageConfigChecker.Coerce(attrs, nil) 72 if err != nil { 73 return nil, errors.Annotate(err, "validating MAAS storage config") 74 } 75 coerced := out.(map[string]interface{}) 76 var tags []string 77 switch v := coerced[tagsAttribute].(type) { 78 case []string: 79 tags = v 80 case string: 81 fields := strings.Split(v, ",") 82 for _, f := range fields { 83 f = strings.TrimSpace(f) 84 if len(f) == 0 { 85 continue 86 } 87 if i := strings.IndexFunc(f, unicode.IsSpace); i >= 0 { 88 return nil, errors.Errorf("tags may not contain whitespace: %q", f) 89 } 90 tags = append(tags, f) 91 } 92 } 93 return &storageConfig{tags: tags}, nil 94 } 95 96 func (maasStorageProvider) ValidateForK8s(map[string]any) error { 97 // no validation required 98 return nil 99 } 100 101 // ValidateConfig is defined on the Provider interface. 102 func (maasStorageProvider) ValidateConfig(cfg *storage.Config) error { 103 _, err := newStorageConfig(cfg.Attrs()) 104 return errors.Trace(err) 105 } 106 107 // Supports is defined on the Provider interface. 108 func (maasStorageProvider) Supports(k storage.StorageKind) bool { 109 return k == storage.StorageKindBlock 110 } 111 112 // Scope is defined on the Provider interface. 113 func (maasStorageProvider) Scope() storage.Scope { 114 return storage.ScopeEnviron 115 } 116 117 // Dynamic is defined on the Provider interface. 118 func (maasStorageProvider) Dynamic() bool { 119 return false 120 } 121 122 // Releasable is defined on the Provider interface. 123 func (maasStorageProvider) Releasable() bool { 124 return false 125 } 126 127 // DefaultPools is defined on the Provider interface. 128 func (maasStorageProvider) DefaultPools() []*storage.Config { 129 return nil 130 } 131 132 // VolumeSource is defined on the Provider interface. 133 func (maasStorageProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { 134 // Dynamic volumes not supported. 135 return nil, errors.NotSupportedf("volumes") 136 } 137 138 // FilesystemSource is defined on the Provider interface. 139 func (maasStorageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 140 return nil, errors.NotSupportedf("filesystems") 141 } 142 143 type volumeInfo struct { 144 name string 145 sizeInGB uint64 146 tags []string 147 } 148 149 // mibToGB converts the value in MiB to GB. 150 // Juju works in MiB, MAAS expects GB. 151 func mibToGb(m uint64) uint64 { 152 return common.MiBToGiB(m) * (humanize.GiByte / humanize.GByte) 153 } 154 155 // buildMAASVolumeParameters creates the MAAS volume information to include 156 // in a request to acquire a MAAS node, based on the supplied storage parameters. 157 func buildMAASVolumeParameters(args []storage.VolumeParams, cons constraints.Value) ([]volumeInfo, error) { 158 if len(args) == 0 && cons.RootDisk == nil { 159 return nil, nil 160 } 161 volumes := make([]volumeInfo, len(args)+1) 162 rootVolume := volumeInfo{name: rootDiskLabel} 163 if cons.RootDisk != nil { 164 rootVolume.sizeInGB = mibToGb(*cons.RootDisk) 165 } 166 volumes[0] = rootVolume 167 for i, v := range args { 168 cfg, err := newStorageConfig(v.Attributes) 169 if err != nil { 170 return nil, errors.Trace(err) 171 } 172 info := volumeInfo{ 173 name: v.Tag.Id(), 174 sizeInGB: mibToGb(v.Size), 175 tags: cfg.tags, 176 } 177 volumes[i+1] = info 178 } 179 return volumes, nil 180 } 181 182 func (mi *maasInstance) volumes( 183 mTag names.MachineTag, requestedVolumes []names.VolumeTag, 184 ) ( 185 []storage.Volume, []storage.VolumeAttachment, error, 186 ) { 187 if mi.constraintMatches.Storage == nil { 188 return nil, nil, errors.NotFoundf("constraint storage mapping") 189 } 190 191 var volumes []storage.Volume 192 var attachments []storage.VolumeAttachment 193 194 // Set up a collection of volumes tags which 195 // we specifically asked for when the node was acquired. 196 validVolumes := set.NewStrings() 197 for _, v := range requestedVolumes { 198 validVolumes.Add(v.Id()) 199 } 200 201 for label, devices := range mi.constraintMatches.Storage { 202 // We don't explicitly allow the root volume to be specified yet. 203 if label == rootDiskLabel { 204 continue 205 } 206 207 // We only care about the volumes we specifically asked for. 208 if !validVolumes.Contains(label) { 209 continue 210 } 211 212 // There should be exactly one block device per label. 213 if len(devices) == 0 { 214 continue 215 } else if len(devices) > 1 { 216 // This should never happen, as we only request one block 217 // device per label. If it does happen, we'll just report 218 // the first block device and log this warning. 219 logger.Warningf( 220 "expected 1 block device for label %s, received %d", 221 label, len(devices), 222 ) 223 } 224 225 device := devices[0] 226 volumeTag := names.NewVolumeTag(label) 227 vol := storage.Volume{ 228 volumeTag, 229 storage.VolumeInfo{ 230 VolumeId: volumeTag.String(), 231 Size: device.Size() / humanize.MiByte, 232 Persistent: false, 233 }, 234 } 235 attachment := storage.VolumeAttachment{ 236 volumeTag, 237 mTag, 238 storage.VolumeAttachmentInfo{ 239 ReadOnly: false, 240 }, 241 } 242 243 const devDiskByIdPrefix = "/dev/disk/by-id/" 244 const devPrefix = "/dev/" 245 246 if blockDev, ok := device.(gomaasapi.BlockDevice); ok { 247 // Handle a block device specifically that way the path used 248 // by Juju will always be a persistent path. 249 idPath := blockDev.IDPath() 250 if idPath == devPrefix+blockDev.Name() { 251 // On vMAAS (i.e. with virtio), the device name 252 // will be stable, and is what is used to form 253 // id_path. 254 deviceName := idPath[len(devPrefix):] 255 attachment.DeviceName = deviceName 256 } else if strings.HasPrefix(idPath, devDiskByIdPrefix) { 257 const wwnPrefix = "wwn-" 258 id := idPath[len(devDiskByIdPrefix):] 259 if strings.HasPrefix(id, wwnPrefix) { 260 vol.WWN = id[len(wwnPrefix):] 261 } else { 262 vol.HardwareId = id 263 } 264 } else { 265 // It's neither /dev/<name> nor /dev/disk/by-id/<hardware-id>, 266 // so set it as the device link and hope for 267 // the best. At worst, the path won't exist 268 // and the storage will remain pending. 269 attachment.DeviceLink = idPath 270 } 271 } else { 272 // Handle all other storage devices using the path MAAS provided. 273 // In the case of partitions the path is always stable because its 274 // based on the GUID of the partition using the dname path. 275 attachment.DeviceLink = device.Path() 276 } 277 278 volumes = append(volumes, vol) 279 attachments = append(attachments, attachment) 280 } 281 return volumes, attachments, nil 282 }