github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/gce/disks.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package gce 5 6 import ( 7 "fmt" 8 "strings" 9 "sync" 10 11 "github.com/juju/collections/set" 12 "github.com/juju/errors" 13 "github.com/juju/utils/v3" 14 15 "github.com/juju/juju/environs/context" 16 "github.com/juju/juju/environs/tags" 17 "github.com/juju/juju/provider/gce/google" 18 "github.com/juju/juju/storage" 19 ) 20 21 const ( 22 storageProviderType = storage.ProviderType("gce") 23 ) 24 25 // StorageProviderTypes implements storage.ProviderRegistry. 26 func (env *environ) StorageProviderTypes() ([]storage.ProviderType, error) { 27 return []storage.ProviderType{storageProviderType}, nil 28 } 29 30 // StorageProvider implements storage.ProviderRegistry. 31 func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 32 if t == storageProviderType { 33 return &storageProvider{env}, nil 34 } 35 return nil, errors.NotFoundf("storage provider %q", t) 36 } 37 38 type storageProvider struct { 39 env *environ 40 } 41 42 var _ storage.Provider = (*storageProvider)(nil) 43 44 func (g *storageProvider) ValidateForK8s(map[string]any) error { 45 // no validation required 46 return nil 47 } 48 49 func (g *storageProvider) ValidateConfig(cfg *storage.Config) error { 50 return nil 51 } 52 53 func (g *storageProvider) Supports(k storage.StorageKind) bool { 54 return k == storage.StorageKindBlock 55 } 56 57 func (g *storageProvider) Scope() storage.Scope { 58 return storage.ScopeEnviron 59 } 60 61 func (g *storageProvider) Dynamic() bool { 62 return true 63 } 64 65 func (e *storageProvider) Releasable() bool { 66 return true 67 } 68 69 func (g *storageProvider) DefaultPools() []*storage.Config { 70 // TODO(perrito666) Add explicit pools. 71 return nil 72 } 73 74 func (g *storageProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 75 return nil, errors.NotSupportedf("filesystems") 76 } 77 78 type volumeSource struct { 79 gce gceConnection 80 envName string // non-unique, informational only 81 modelUUID string 82 } 83 84 func (g *storageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { 85 environConfig := g.env.Config() 86 source := &volumeSource{ 87 gce: g.env.gce, 88 envName: environConfig.Name(), 89 modelUUID: environConfig.UUID(), 90 } 91 return source, nil 92 } 93 94 type instanceCache map[string]google.Instance 95 96 func (c instanceCache) update(gceClient gceConnection, ctx context.ProviderCallContext, ids ...string) error { 97 if len(ids) == 1 { 98 if _, ok := c[ids[0]]; ok { 99 return nil 100 } 101 } 102 idMap := make(map[string]int, len(ids)) 103 for _, id := range ids { 104 idMap[id] = 0 105 } 106 instances, err := gceClient.Instances("", google.StatusRunning) 107 if err != nil { 108 return google.HandleCredentialError(errors.Annotate(err, "querying instance details"), ctx) 109 } 110 for _, instance := range instances { 111 if _, ok := idMap[instance.ID]; !ok { 112 continue 113 } 114 c[instance.ID] = instance 115 } 116 return nil 117 } 118 119 func (c instanceCache) get(id string) (google.Instance, error) { 120 inst, ok := c[id] 121 if !ok { 122 return google.Instance{}, errors.Errorf("cannot attach to non-running instance %v", id) 123 } 124 return inst, nil 125 } 126 127 func (v *volumeSource) CreateVolumes(ctx context.ProviderCallContext, params []storage.VolumeParams) (_ []storage.CreateVolumesResult, err error) { 128 results := make([]storage.CreateVolumesResult, len(params)) 129 instanceIds := set.NewStrings() 130 for i, p := range params { 131 if err := v.ValidateVolumeParams(p); err != nil { 132 results[i].Error = err 133 continue 134 } 135 instanceIds.Add(string(p.Attachment.InstanceId)) 136 } 137 138 instances := make(instanceCache) 139 if instanceIds.Size() > 1 { 140 if err := instances.update(v.gce, ctx, instanceIds.Values()...); err != nil { 141 logger.Debugf("querying running instances: %v", err) 142 // We ignore the error, because we don't want an invalid 143 // InstanceId reference from one VolumeParams to prevent 144 // the creation of another volume. 145 // ... Unless the error is due to an invalid credential, in which case, continuing with this call 146 // is pointless and creates an unnecessary churn: we know all calls will fail with the same error. 147 if google.HasDenialStatusCode(err) { 148 return results, err 149 } 150 } 151 } 152 153 for i, p := range params { 154 if results[i].Error != nil { 155 continue 156 } 157 volume, attachment, err := v.createOneVolume(ctx, p, instances) 158 if err != nil { 159 results[i].Error = err 160 logger.Errorf("could not create one volume (or attach it): %v", err) 161 // ... Unless the error is due to an invalid credential, in which case, continuing with this call 162 // is pointless and creates an unnecessary churn: we know all calls will fail with the same error. 163 if google.HasDenialStatusCode(err) { 164 return results, err 165 } 166 continue 167 } 168 results[i].Volume = volume 169 results[i].VolumeAttachment = attachment 170 } 171 return results, nil 172 } 173 174 // mibToGib converts mebibytes to gibibytes. 175 // GCE expects GiB, we work in MiB; round up 176 // to nearest GiB. 177 func mibToGib(m uint64) uint64 { 178 return (m + 1023) / 1024 179 } 180 181 func nameVolume(zone string) (string, error) { 182 volumeUUID, err := utils.NewUUID() 183 if err != nil { 184 return "", errors.Annotate(err, "cannot generate uuid to name the volume") 185 } 186 // type-zone-uuid 187 volumeName := fmt.Sprintf("%s--%s", zone, volumeUUID.String()) 188 return volumeName, nil 189 } 190 191 func (v *volumeSource) createOneVolume(ctx context.ProviderCallContext, p storage.VolumeParams, instances instanceCache) (volume *storage.Volume, volumeAttachment *storage.VolumeAttachment, err error) { 192 var volumeName, zone string 193 defer func() { 194 if err == nil || volumeName == "" { 195 return 196 } 197 if err := v.gce.RemoveDisk(zone, volumeName); err != nil { 198 logger.Errorf("error cleaning up volume %v: %v", volumeName, google.HandleCredentialError(err, ctx)) 199 } 200 }() 201 202 instId := string(p.Attachment.InstanceId) 203 if err := instances.update(v.gce, ctx, instId); err != nil { 204 return nil, nil, errors.Annotatef(err, "cannot add %q to instance cache", instId) 205 } 206 inst, err := instances.get(instId) 207 if err != nil { 208 // Can't create the volume without the instance, 209 // because we need to know what its AZ is. 210 return nil, nil, errors.Annotatef(err, "cannot obtain %q from instance cache", instId) 211 } 212 persistentType, ok := p.Attributes["type"].(google.DiskType) 213 if !ok { 214 persistentType = google.DiskPersistentStandard 215 } 216 217 zone = inst.ZoneName 218 volumeName, err = nameVolume(zone) 219 if err != nil { 220 return nil, nil, errors.Annotate(err, "cannot create a new volume name") 221 } 222 // TODO(perrito666) the volumeName is arbitrary and it was crafted this 223 // way to help solve the need to have zone all over the place. 224 disk := google.DiskSpec{ 225 SizeHintGB: mibToGib(p.Size), 226 Name: volumeName, 227 PersistentDiskType: persistentType, 228 Labels: resourceTagsToDiskLabels(p.ResourceTags), 229 } 230 231 gceDisks, err := v.gce.CreateDisks(zone, []google.DiskSpec{disk}) 232 if err != nil { 233 return nil, nil, google.HandleCredentialError(errors.Annotate(err, "cannot create disk"), ctx) 234 } 235 if len(gceDisks) != 1 { 236 return nil, nil, errors.New(fmt.Sprintf("unexpected number of disks created: %d", len(gceDisks))) 237 } 238 gceDisk := gceDisks[0] 239 240 attachedDisk, err := v.attachOneVolume(ctx, gceDisk.Name, google.ModeRW, inst.ID) 241 if err != nil { 242 return nil, nil, errors.Annotatef(err, "attaching %q to %q", gceDisk.Name, instId) 243 } 244 245 volume = &storage.Volume{ 246 p.Tag, 247 storage.VolumeInfo{ 248 VolumeId: gceDisk.Name, 249 Size: gceDisk.Size, 250 Persistent: true, 251 }, 252 } 253 254 volumeAttachment = &storage.VolumeAttachment{ 255 p.Tag, 256 p.Attachment.Machine, 257 storage.VolumeAttachmentInfo{ 258 DeviceLink: fmt.Sprintf( 259 "/dev/disk/by-id/google-%s", 260 attachedDisk.DeviceName, 261 ), 262 }, 263 } 264 265 return volume, volumeAttachment, nil 266 } 267 268 func (v *volumeSource) DestroyVolumes(ctx context.ProviderCallContext, volNames []string) ([]error, error) { 269 return v.foreachVolume(ctx, volNames, v.destroyOneVolume), nil 270 } 271 272 func (v *volumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volNames []string) ([]error, error) { 273 return v.foreachVolume(ctx, volNames, v.releaseOneVolume), nil 274 } 275 276 func (v *volumeSource) foreachVolume(ctx context.ProviderCallContext, volNames []string, f func(context.ProviderCallContext, string) error) []error { 277 var wg sync.WaitGroup 278 wg.Add(len(volNames)) 279 results := make([]error, len(volNames)) 280 for i, volumeName := range volNames { 281 go func(i int, volumeName string) { 282 defer wg.Done() 283 results[i] = f(ctx, volumeName) 284 }(i, volumeName) 285 } 286 wg.Wait() 287 return results 288 } 289 290 func parseVolumeId(volName string) (string, string, error) { 291 idRest := strings.SplitN(volName, "--", 2) 292 if len(idRest) != 2 { 293 return "", "", errors.New(fmt.Sprintf("malformed volume id %q", volName)) 294 } 295 zone := idRest[0] 296 volumeUUID := idRest[1] 297 return zone, volumeUUID, nil 298 } 299 300 func isValidVolume(volumeName string) bool { 301 _, _, err := parseVolumeId(volumeName) 302 return err == nil 303 } 304 305 func (v *volumeSource) destroyOneVolume(ctx context.ProviderCallContext, volName string) error { 306 zone, _, err := parseVolumeId(volName) 307 if err != nil { 308 return errors.Annotatef(err, "invalid volume id %q", volName) 309 } 310 if err := v.gce.RemoveDisk(zone, volName); err != nil { 311 return google.HandleCredentialError(errors.Annotatef(err, "cannot destroy volume %q", volName), ctx) 312 } 313 return nil 314 } 315 316 func (v *volumeSource) releaseOneVolume(ctx context.ProviderCallContext, volName string) error { 317 zone, _, err := parseVolumeId(volName) 318 if err != nil { 319 return errors.Annotatef(err, "invalid volume id %q", volName) 320 } 321 disk, err := v.gce.Disk(zone, volName) 322 if err != nil { 323 return google.HandleCredentialError(errors.Trace(err), ctx) 324 } 325 switch disk.Status { 326 case google.StatusReady, google.StatusFailed: 327 default: 328 return errors.Errorf( 329 "cannot release volume %q with status %q", 330 volName, disk.Status, 331 ) 332 } 333 if len(disk.AttachedInstances) > 0 { 334 return errors.Errorf( 335 "cannot release volume %q, attached to instances %q", 336 volName, disk.AttachedInstances, 337 ) 338 } 339 delete(disk.Labels, tags.JujuController) 340 delete(disk.Labels, tags.JujuModel) 341 if err := v.gce.SetDiskLabels(zone, volName, disk.LabelFingerprint, disk.Labels); err != nil { 342 return google.HandleCredentialError(errors.Annotatef(err, "cannot remove labels from volume %q", volName), ctx) 343 } 344 return nil 345 } 346 347 func (v *volumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 348 var volumes []string 349 disks, err := v.gce.Disks() 350 if err != nil { 351 return nil, google.HandleCredentialError(errors.Trace(err), ctx) 352 } 353 for _, disk := range disks { 354 if !isValidVolume(disk.Name) { 355 continue 356 } 357 if disk.Labels[tags.JujuModel] != v.modelUUID { 358 continue 359 } 360 volumes = append(volumes, disk.Name) 361 } 362 return volumes, nil 363 } 364 365 // ImportVolume is specified on the storage.VolumeImporter interface. 366 func (v *volumeSource) ImportVolume(ctx context.ProviderCallContext, volName string, tags map[string]string) (storage.VolumeInfo, error) { 367 zone, _, err := parseVolumeId(volName) 368 if err != nil { 369 return storage.VolumeInfo{}, errors.Annotatef(err, "cannot get volume %q", volName) 370 } 371 disk, err := v.gce.Disk(zone, volName) 372 if err != nil { 373 return storage.VolumeInfo{}, google.HandleCredentialError(errors.Annotatef(err, "cannot get volume %q", volName), ctx) 374 } 375 if disk.Status != google.StatusReady { 376 return storage.VolumeInfo{}, errors.Errorf( 377 "cannot import volume %q with status %q", 378 volName, disk.Status, 379 ) 380 } 381 if disk.Labels == nil { 382 disk.Labels = make(map[string]string) 383 } 384 for k, v := range resourceTagsToDiskLabels(tags) { 385 disk.Labels[k] = v 386 } 387 if err := v.gce.SetDiskLabels(zone, volName, disk.LabelFingerprint, disk.Labels); err != nil { 388 return storage.VolumeInfo{}, google.HandleCredentialError(errors.Annotatef(err, "cannot update labels on volume %q", volName), ctx) 389 } 390 return storage.VolumeInfo{ 391 VolumeId: disk.Name, 392 Size: disk.Size, 393 Persistent: true, 394 }, nil 395 } 396 397 func (v *volumeSource) DescribeVolumes(ctx context.ProviderCallContext, volNames []string) ([]storage.DescribeVolumesResult, error) { 398 results := make([]storage.DescribeVolumesResult, len(volNames)) 399 for i, vol := range volNames { 400 res, err := v.describeOneVolume(ctx, vol) 401 if err != nil { 402 return nil, errors.Annotate(err, "cannot describe volumes") 403 } 404 results[i] = res 405 } 406 return results, nil 407 } 408 409 func (v *volumeSource) describeOneVolume(ctx context.ProviderCallContext, volName string) (storage.DescribeVolumesResult, error) { 410 zone, _, err := parseVolumeId(volName) 411 if err != nil { 412 return storage.DescribeVolumesResult{}, errors.Annotatef(err, "cannot describe %q", volName) 413 } 414 disk, err := v.gce.Disk(zone, volName) 415 if err != nil { 416 return storage.DescribeVolumesResult{}, google.HandleCredentialError(errors.Annotatef(err, "cannot get volume %q", volName), ctx) 417 } 418 desc := storage.DescribeVolumesResult{ 419 &storage.VolumeInfo{ 420 Size: disk.Size, 421 VolumeId: disk.Name, 422 }, 423 nil, 424 } 425 return desc, nil 426 } 427 428 // TODO(perrito666) These rules are yet to be defined. 429 func (v *volumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 430 return nil 431 } 432 433 func (v *volumeSource) AttachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 434 results := make([]storage.AttachVolumesResult, len(attachParams)) 435 for i, attachment := range attachParams { 436 volumeName := attachment.VolumeId 437 mode := google.ModeRW 438 if attachment.ReadOnly { 439 mode = google.ModeRW 440 } 441 instanceId := attachment.InstanceId 442 attached, err := v.attachOneVolume(ctx, volumeName, mode, string(instanceId)) 443 if err != nil { 444 logger.Errorf("could not attach %q to %q: %v", volumeName, instanceId, err) 445 results[i].Error = err 446 // ... Unless the error is due to an invalid credential, in which case, continuing with this call 447 // is pointless and creates an unnecessary churn: we know all calls will fail with the same error. 448 if google.HasDenialStatusCode(err) { 449 return results, err 450 } 451 continue 452 } 453 results[i].VolumeAttachment = &storage.VolumeAttachment{ 454 attachment.Volume, 455 attachment.Machine, 456 storage.VolumeAttachmentInfo{ 457 DeviceLink: fmt.Sprintf( 458 "/dev/disk/by-id/google-%s", 459 attached.DeviceName, 460 ), 461 }, 462 } 463 } 464 return results, nil 465 } 466 467 func (v *volumeSource) attachOneVolume(ctx context.ProviderCallContext, volumeName string, mode google.DiskMode, instanceId string) (*google.AttachedDisk, error) { 468 zone, _, err := parseVolumeId(volumeName) 469 if err != nil { 470 return nil, errors.Annotate(err, "invalid volume name") 471 } 472 instanceDisks, err := v.gce.InstanceDisks(zone, instanceId) 473 if err != nil { 474 return nil, google.HandleCredentialError(errors.Annotate(err, "cannot verify if the disk is already in the instance"), ctx) 475 } 476 // Is it already attached? 477 for _, disk := range instanceDisks { 478 if disk.VolumeName == volumeName { 479 return disk, nil 480 } 481 } 482 483 attachment, err := v.gce.AttachDisk(zone, volumeName, instanceId, mode) 484 if err != nil { 485 return nil, google.HandleCredentialError(errors.Annotate(err, "cannot attach volume"), ctx) 486 } 487 return attachment, nil 488 } 489 490 func (v *volumeSource) DetachVolumes(ctx context.ProviderCallContext, attachParams []storage.VolumeAttachmentParams) ([]error, error) { 491 result := make([]error, len(attachParams)) 492 for i, volumeAttachment := range attachParams { 493 err := v.detachOneVolume(ctx, volumeAttachment) 494 if google.HasDenialStatusCode(err) { 495 // no need to continue as we'll keep getting the same invalid credential error. 496 return result, err 497 } 498 result[i] = err 499 } 500 return result, nil 501 } 502 503 func (v *volumeSource) detachOneVolume(ctx context.ProviderCallContext, attachParam storage.VolumeAttachmentParams) error { 504 instId := attachParam.InstanceId 505 volumeName := attachParam.VolumeId 506 zone, _, err := parseVolumeId(volumeName) 507 if err != nil { 508 return errors.Annotatef(err, "%q is not a valid volume id", volumeName) 509 } 510 return google.HandleCredentialError(v.gce.DetachDisk(zone, string(instId), volumeName), ctx) 511 } 512 513 // resourceTagsToDiskLabels translates a set of 514 // resource tags, provided by Juju, to disk labels. 515 func resourceTagsToDiskLabels(in map[string]string) map[string]string { 516 out := make(map[string]string) 517 for k, v := range in { 518 // Only the controller and model UUID tags are carried 519 // over, as they're known not to conflict with GCE's 520 // rules regarding label values. 521 switch k { 522 case tags.JujuController, tags.JujuModel: 523 out[k] = v 524 } 525 } 526 return out 527 }