github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/provider/lxd/storage.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/canonical/lxd/shared/api" 11 "github.com/canonical/lxd/shared/units" 12 "github.com/juju/collections/set" 13 "github.com/juju/errors" 14 "github.com/juju/names/v5" 15 "github.com/juju/schema" 16 17 "github.com/juju/juju/container/lxd" 18 "github.com/juju/juju/core/instance" 19 "github.com/juju/juju/environs" 20 "github.com/juju/juju/environs/context" 21 "github.com/juju/juju/environs/tags" 22 "github.com/juju/juju/provider/common" 23 "github.com/juju/juju/storage" 24 ) 25 26 const ( 27 lxdStorageProviderType = "lxd" 28 29 // attrLXDStorageDriver is the attribute name for the 30 // storage pool's LXD storage driver. This and "lxd-pool" 31 // are the only predefined storage attributes; all others 32 // are passed on to LXD directly. 33 attrLXDStorageDriver = "driver" 34 35 // attrLXDStoragePool is the attribute name for the 36 // storage pool's corresponding LXD storage pool name. 37 // If this is not provided, the LXD storage pool name 38 // will be set to "juju". 39 attrLXDStoragePool = "lxd-pool" 40 41 storagePoolVolumeType = "custom" 42 ) 43 44 func (env *environ) storageSupported() bool { 45 return env.server().StorageSupported() 46 } 47 48 // StorageProviderTypes implements storage.ProviderRegistry. 49 func (env *environ) StorageProviderTypes() ([]storage.ProviderType, error) { 50 var types []storage.ProviderType 51 if env.storageSupported() { 52 types = append(types, lxdStorageProviderType) 53 } 54 return types, nil 55 } 56 57 // StorageProvider implements storage.ProviderRegistry. 58 func (env *environ) StorageProvider(t storage.ProviderType) (storage.Provider, error) { 59 if env.storageSupported() && t == lxdStorageProviderType { 60 return &lxdStorageProvider{env}, nil 61 } 62 return nil, errors.NotFoundf("storage provider %q", t) 63 } 64 65 // lxdStorageProvider is a storage provider for LXD volumes, exposed to Juju as 66 // filesystems. 67 type lxdStorageProvider struct { 68 env *environ 69 } 70 71 var _ storage.Provider = (*lxdStorageProvider)(nil) 72 73 var lxdStorageConfigFields = schema.Fields{ 74 attrLXDStorageDriver: schema.OneOf( 75 schema.Const("zfs"), 76 schema.Const("dir"), 77 schema.Const("btrfs"), 78 schema.Const("lvm"), 79 ), 80 attrLXDStoragePool: schema.String(), 81 } 82 83 var lxdStorageConfigChecker = schema.FieldMap( 84 lxdStorageConfigFields, 85 schema.Defaults{ 86 attrLXDStorageDriver: "dir", 87 attrLXDStoragePool: schema.Omit, 88 }, 89 ) 90 91 type lxdStorageConfig struct { 92 lxdPool string 93 driver string 94 attrs map[string]string 95 } 96 97 func newLXDStorageConfig(attrs map[string]interface{}) (*lxdStorageConfig, error) { 98 coerced, err := lxdStorageConfigChecker.Coerce(attrs, nil) 99 if err != nil { 100 return nil, errors.Annotate(err, "validating LXD storage config") 101 } 102 attrs = coerced.(map[string]interface{}) 103 104 driver := attrs[attrLXDStorageDriver].(string) 105 lxdPool, _ := attrs[attrLXDStoragePool].(string) 106 delete(attrs, attrLXDStorageDriver) 107 delete(attrs, attrLXDStoragePool) 108 109 var stringAttrs map[string]string 110 if len(attrs) > 0 { 111 stringAttrs = make(map[string]string) 112 for k, v := range attrs { 113 if vString, ok := v.(string); ok { 114 stringAttrs[k] = vString 115 } else { 116 stringAttrs[k] = fmt.Sprint(v) 117 } 118 } 119 } 120 121 if lxdPool == "" { 122 lxdPool = "juju" 123 } 124 125 lxdStorageConfig := &lxdStorageConfig{ 126 lxdPool: lxdPool, 127 driver: driver, 128 attrs: stringAttrs, 129 } 130 return lxdStorageConfig, nil 131 } 132 133 func (e *lxdStorageProvider) ValidateForK8s(map[string]any) error { 134 return errors.NotValidf("storage provider type %q", lxdStorageProviderType) 135 } 136 137 // ValidateConfig is part of the Provider interface. 138 func (e *lxdStorageProvider) ValidateConfig(cfg *storage.Config) error { 139 lxdStorageConfig, err := newLXDStorageConfig(cfg.Attrs()) 140 if err != nil { 141 return errors.Trace(err) 142 } 143 return ensureLXDStoragePool(e.env, lxdStorageConfig) 144 } 145 146 // Supports is part of the Provider interface. 147 func (e *lxdStorageProvider) Supports(k storage.StorageKind) bool { 148 return k == storage.StorageKindFilesystem 149 } 150 151 // Scope is part of the Provider interface. 152 func (e *lxdStorageProvider) Scope() storage.Scope { 153 return storage.ScopeEnviron 154 } 155 156 // Dynamic is part of the Provider interface. 157 func (e *lxdStorageProvider) Dynamic() bool { 158 return true 159 } 160 161 // Releasable is defined on the Provider interface. 162 func (*lxdStorageProvider) Releasable() bool { 163 return true 164 } 165 166 // DefaultPools is part of the Provider interface. 167 func (e *lxdStorageProvider) DefaultPools() []*storage.Config { 168 zfsPool, _ := storage.NewConfig("lxd-zfs", lxdStorageProviderType, map[string]interface{}{ 169 attrLXDStorageDriver: "zfs", 170 attrLXDStoragePool: "juju-zfs", 171 "zfs.pool_name": "juju-lxd", 172 }) 173 btrfsPool, _ := storage.NewConfig("lxd-btrfs", lxdStorageProviderType, map[string]interface{}{ 174 attrLXDStorageDriver: "btrfs", 175 attrLXDStoragePool: "juju-btrfs", 176 }) 177 178 var pools []*storage.Config 179 if e.ValidateConfig(zfsPool) == nil { 180 pools = append(pools, zfsPool) 181 } 182 if e.ValidateConfig(btrfsPool) == nil { 183 pools = append(pools, btrfsPool) 184 } 185 return pools 186 } 187 188 // VolumeSource is part of the Provider interface. 189 func (e *lxdStorageProvider) VolumeSource(cfg *storage.Config) (storage.VolumeSource, error) { 190 return nil, errors.NotSupportedf("volumes") 191 } 192 193 // FilesystemSource is part of the Provider interface. 194 func (e *lxdStorageProvider) FilesystemSource(cfg *storage.Config) (storage.FilesystemSource, error) { 195 return &lxdFilesystemSource{e.env}, nil 196 } 197 198 func ensureLXDStoragePool(env *environ, cfg *lxdStorageConfig) error { 199 server := env.server() 200 createErr := server.CreatePool(cfg.lxdPool, cfg.driver, cfg.attrs) 201 if createErr == nil { 202 return nil 203 } 204 // There's no specific error to check for, so we just assume 205 // that the error is due to the pool already existing, and 206 // verify that. If it doesn't exist, return the original 207 // CreateStoragePool error. 208 209 pool, _, err := server.GetStoragePool(cfg.lxdPool) 210 if lxd.IsLXDNotFound(err) { 211 return errors.Annotatef(createErr, "creating LXD storage pool %q", cfg.lxdPool) 212 } else if err != nil { 213 return errors.Annotatef(createErr, "getting storage pool %q", cfg.lxdPool) 214 } 215 // The storage pool already exists: check that the existing pool's 216 // driver and config match what we want. 217 if pool.Driver != cfg.driver { 218 return errors.Errorf( 219 `LXD storage pool %q exists, with conflicting driver %q. Specify an alternative pool name via the "lxd-pool" attribute.`, 220 pool.Name, pool.Driver, 221 ) 222 } 223 for k, v := range cfg.attrs { 224 if haveV, ok := pool.Config[k]; !ok || haveV != v { 225 return errors.Errorf( 226 `LXD storage pool %q exists, with conflicting config attribute %q=%q. Specify an alternative pool name via the "lxd-pool" attribute.`, 227 pool.Name, k, haveV, 228 ) 229 } 230 } 231 return nil 232 } 233 234 type lxdFilesystemSource struct { 235 env *environ 236 } 237 238 // CreateFilesystems is specified on the storage.FilesystemSource interface. 239 func (s *lxdFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) (_ []storage.CreateFilesystemsResult, err error) { 240 results := make([]storage.CreateFilesystemsResult, len(args)) 241 for i, arg := range args { 242 if err := s.ValidateFilesystemParams(arg); err != nil { 243 results[i].Error = err 244 continue 245 } 246 filesystem, err := s.createFilesystem(arg) 247 if err != nil { 248 results[i].Error = err 249 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 250 continue 251 } 252 results[i].Filesystem = filesystem 253 } 254 return results, nil 255 } 256 257 func (s *lxdFilesystemSource) createFilesystem( 258 arg storage.FilesystemParams, 259 ) (*storage.Filesystem, error) { 260 261 cfg, err := newLXDStorageConfig(arg.Attributes) 262 if err != nil { 263 return nil, errors.Trace(err) 264 } 265 if err := ensureLXDStoragePool(s.env, cfg); err != nil { 266 return nil, errors.Trace(err) 267 } 268 269 // The filesystem ID needs to be something unique, since there 270 // could be multiple models creating volumes within the same 271 // LXD storage pool. 272 volumeName := s.env.namespace.Value(arg.Tag.String()) 273 filesystemId := makeFilesystemId(cfg, volumeName) 274 275 config := map[string]string{} 276 for k, v := range arg.ResourceTags { 277 config["user."+k] = v 278 } 279 switch cfg.driver { 280 case "dir": 281 // NOTE(axw) for the "dir" driver, the size attribute is rejected 282 // by LXD. Ideally LXD would be able to tell us the total size of 283 // the filesystem on which the directory was created, though. 284 default: 285 config["size"] = fmt.Sprintf("%dMiB", arg.Size) 286 } 287 288 if err := s.env.server().CreateVolume(cfg.lxdPool, volumeName, config); err != nil { 289 return nil, errors.Annotate(err, "creating volume") 290 } 291 292 filesystem := storage.Filesystem{ 293 Tag: arg.Tag, 294 FilesystemInfo: storage.FilesystemInfo{ 295 FilesystemId: filesystemId, 296 Size: arg.Size, 297 }, 298 } 299 return &filesystem, nil 300 } 301 302 func makeFilesystemId(cfg *lxdStorageConfig, volumeName string) string { 303 // We need to include the LXD pool name in the filesystem ID, 304 // so that we can map it back to a volume. 305 return fmt.Sprintf("%s:%s", cfg.lxdPool, volumeName) 306 } 307 308 // parseFilesystemId parses the given filesystem ID, returning the underlying 309 // LXD storage pool name and volume name. 310 func parseFilesystemId(id string) (lxdPool, volumeName string, _ error) { 311 fields := strings.SplitN(id, ":", 2) 312 if len(fields) < 2 { 313 return "", "", errors.Errorf( 314 "invalid filesystem ID %q; expected ID in format <lxd-pool>:<volume-name>", id, 315 ) 316 } 317 return fields[0], fields[1], nil 318 } 319 320 // TODO (manadart 2018-06-28) Add a test for DestroyController that properly 321 // verifies this behaviour. 322 func destroyControllerFilesystems(env *environ, controllerUUID string) error { 323 return errors.Trace(destroyFilesystems(env, func(v api.StorageVolume) bool { 324 return v.Config["user."+tags.JujuController] == controllerUUID 325 })) 326 } 327 328 func destroyModelFilesystems(env *environ) error { 329 return errors.Trace(destroyFilesystems(env, func(v api.StorageVolume) bool { 330 return v.Config["user."+tags.JujuModel] == env.Config().UUID() 331 })) 332 } 333 334 func destroyFilesystems(env *environ, match func(api.StorageVolume) bool) error { 335 server := env.server() 336 pools, err := server.GetStoragePools() 337 if err != nil { 338 return errors.Annotate(err, "listing LXD storage pools") 339 } 340 for _, pool := range pools { 341 volumes, err := server.GetStoragePoolVolumes(pool.Name) 342 if err != nil { 343 return errors.Annotatef(err, "listing volumes in LXD storage pool %q", pool) 344 } 345 for _, volume := range volumes { 346 if !match(volume) { 347 continue 348 } 349 if err := server.DeleteStoragePoolVolume(pool.Name, storagePoolVolumeType, volume.Name); err != nil { 350 return errors.Annotatef(err, "deleting volume %q in LXD storage pool %q", volume.Name, pool) 351 } 352 } 353 } 354 return nil 355 } 356 357 // DestroyFilesystems is specified on the storage.FilesystemSource interface. 358 func (s *lxdFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 359 results := make([]error, len(filesystemIds)) 360 for i, filesystemId := range filesystemIds { 361 results[i] = s.destroyFilesystem(filesystemId) 362 common.HandleCredentialError(IsAuthorisationFailure, results[i], ctx) 363 } 364 return results, nil 365 } 366 367 func (s *lxdFilesystemSource) destroyFilesystem(filesystemId string) error { 368 poolName, volumeName, err := parseFilesystemId(filesystemId) 369 if err != nil { 370 return errors.Trace(err) 371 } 372 err = s.env.server().DeleteStoragePoolVolume(poolName, storagePoolVolumeType, volumeName) 373 if err != nil && !lxd.IsLXDNotFound(err) { 374 return errors.Trace(err) 375 } 376 return nil 377 } 378 379 // ReleaseFilesystems is specified on the storage.FilesystemSource interface. 380 func (s *lxdFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 381 results := make([]error, len(filesystemIds)) 382 for i, filesystemId := range filesystemIds { 383 results[i] = s.releaseFilesystem(filesystemId) 384 common.HandleCredentialError(IsAuthorisationFailure, results[i], ctx) 385 } 386 return results, nil 387 } 388 389 func (s *lxdFilesystemSource) releaseFilesystem(filesystemId string) error { 390 poolName, volumeName, err := parseFilesystemId(filesystemId) 391 if err != nil { 392 return errors.Trace(err) 393 } 394 server := s.env.server() 395 volume, eTag, err := server.GetStoragePoolVolume(poolName, storagePoolVolumeType, volumeName) 396 if err != nil { 397 return errors.Trace(err) 398 } 399 if volume.Config != nil { 400 delete(volume.Config, "user."+tags.JujuModel) 401 delete(volume.Config, "user."+tags.JujuController) 402 if err := server.UpdateStoragePoolVolume( 403 poolName, storagePoolVolumeType, volumeName, volume.Writable(), eTag); err != nil { 404 return errors.Annotatef( 405 err, "removing tags from volume %q in pool %q", 406 volumeName, poolName, 407 ) 408 } 409 } 410 return nil 411 } 412 413 // ValidateFilesystemParams is specified on the storage.FilesystemSource interface. 414 func (s *lxdFilesystemSource) ValidateFilesystemParams(params storage.FilesystemParams) error { 415 // TODO(axw) sanity check params 416 return nil 417 } 418 419 // AttachFilesystems is specified on the storage.FilesystemSource interface. 420 func (s *lxdFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) { 421 var instanceIds []instance.Id 422 instanceIdsSeen := make(set.Strings) 423 for _, arg := range args { 424 if instanceIdsSeen.Contains(string(arg.InstanceId)) { 425 continue 426 } 427 instanceIdsSeen.Add(string(arg.InstanceId)) 428 instanceIds = append(instanceIds, arg.InstanceId) 429 } 430 instances, err := s.env.Instances(ctx, instanceIds) 431 switch err { 432 case nil, environs.ErrPartialInstances, environs.ErrNoInstances: 433 default: 434 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 435 return nil, errors.Trace(err) 436 } 437 438 results := make([]storage.AttachFilesystemsResult, len(args)) 439 for i, arg := range args { 440 var inst *environInstance 441 for i, instanceId := range instanceIds { 442 if instanceId != arg.InstanceId { 443 continue 444 } 445 if instances[i] != nil { 446 inst = instances[i].(*environInstance) 447 } 448 break 449 } 450 attachment, err := s.attachFilesystem(arg, inst) 451 if err != nil { 452 results[i].Error = errors.Annotatef( 453 err, "attaching %s to %s", 454 names.ReadableString(arg.Filesystem), 455 names.ReadableString(arg.Machine), 456 ) 457 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 458 continue 459 } 460 results[i].FilesystemAttachment = attachment 461 } 462 return results, nil 463 } 464 465 func (s *lxdFilesystemSource) attachFilesystem( 466 arg storage.FilesystemAttachmentParams, 467 inst *environInstance, 468 ) (*storage.FilesystemAttachment, error) { 469 if inst == nil { 470 return nil, errors.NotFoundf("instance %q", arg.InstanceId) 471 } 472 473 poolName, volumeName, err := parseFilesystemId(arg.FilesystemId) 474 if err != nil { 475 return nil, errors.Trace(err) 476 } 477 478 deviceName := arg.Filesystem.String() 479 if err = inst.container.AddDisk(deviceName, arg.Path, volumeName, poolName, arg.ReadOnly); err != nil { 480 return nil, errors.Trace(err) 481 } 482 483 if err := s.env.server().WriteContainer(inst.container); err != nil { 484 return nil, errors.Trace(err) 485 } 486 487 filesystemAttachment := storage.FilesystemAttachment{ 488 Filesystem: arg.Filesystem, 489 Machine: arg.Machine, 490 FilesystemAttachmentInfo: storage.FilesystemAttachmentInfo{ 491 Path: arg.Path, 492 ReadOnly: arg.ReadOnly, 493 }, 494 } 495 return &filesystemAttachment, nil 496 } 497 498 // DetachFilesystems is specified on the storage.FilesystemSource interface. 499 func (s *lxdFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) { 500 var instanceIds []instance.Id 501 instanceIdsSeen := make(set.Strings) 502 for _, arg := range args { 503 if instanceIdsSeen.Contains(string(arg.InstanceId)) { 504 continue 505 } 506 instanceIdsSeen.Add(string(arg.InstanceId)) 507 instanceIds = append(instanceIds, arg.InstanceId) 508 } 509 instances, err := s.env.Instances(ctx, instanceIds) 510 switch err { 511 case nil, environs.ErrPartialInstances, environs.ErrNoInstances: 512 default: 513 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 514 return nil, errors.Trace(err) 515 } 516 517 results := make([]error, len(args)) 518 for i, arg := range args { 519 var inst *environInstance 520 for i, instanceId := range instanceIds { 521 if instanceId != arg.InstanceId { 522 continue 523 } 524 if instances[i] != nil { 525 inst = instances[i].(*environInstance) 526 } 527 break 528 } 529 if inst != nil { 530 err := s.detachFilesystem(arg, inst) 531 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 532 results[i] = errors.Annotatef( 533 err, "detaching %s", 534 names.ReadableString(arg.Filesystem), 535 ) 536 } 537 } 538 return results, nil 539 } 540 541 func (s *lxdFilesystemSource) detachFilesystem( 542 arg storage.FilesystemAttachmentParams, 543 inst *environInstance, 544 ) error { 545 deviceName := arg.Filesystem.String() 546 delete(inst.container.Devices, deviceName) 547 return errors.Trace(s.env.server().WriteContainer(inst.container)) 548 } 549 550 // ImportFilesystem is part of the storage.FilesystemImporter interface. 551 func (s *lxdFilesystemSource) ImportFilesystem( 552 callCtx context.ProviderCallContext, 553 filesystemId string, 554 tags map[string]string, 555 ) (storage.FilesystemInfo, error) { 556 lxdPool, volumeName, err := parseFilesystemId(filesystemId) 557 if err != nil { 558 return storage.FilesystemInfo{}, errors.Trace(err) 559 } 560 volume, eTag, err := s.env.server().GetStoragePoolVolume(lxdPool, storagePoolVolumeType, volumeName) 561 if err != nil { 562 common.HandleCredentialError(IsAuthorisationFailure, err, callCtx) 563 return storage.FilesystemInfo{}, errors.Trace(err) 564 } 565 if len(volume.UsedBy) > 0 { 566 return storage.FilesystemInfo{}, errors.Errorf( 567 "filesystem %q is in use by %d containers, cannot import", 568 filesystemId, len(volume.UsedBy), 569 ) 570 } 571 572 // NOTE(axw) not all drivers support specifying a volume size. 573 // If we can't find a size config attribute, we have to make 574 // up a number since the model will not allow a size of zero. 575 // We use the magic number 999GiB to indicate that it's unknown. 576 size := uint64(999 * 1024) // 999GiB 577 if sizeString := volume.Config["size"]; sizeString != "" { 578 n, err := units.ParseByteSizeString(sizeString) 579 if err != nil { 580 return storage.FilesystemInfo{}, errors.Annotate(err, "parsing size") 581 } 582 // ParseByteSizeString returns bytes, we want MiB. 583 size = uint64(n / (1024 * 1024)) 584 } 585 586 if len(tags) > 0 { 587 // Update the volume's user-data with the given tags. This will 588 // include updating the model and controller UUIDs, so that the 589 // storage is associated with this controller and model. 590 if volume.Config == nil { 591 volume.Config = make(map[string]string) 592 } 593 for k, v := range tags { 594 volume.Config["user."+k] = v 595 } 596 if err := s.env.server().UpdateStoragePoolVolume( 597 lxdPool, storagePoolVolumeType, volumeName, volume.Writable(), eTag); err != nil { 598 common.HandleCredentialError(IsAuthorisationFailure, err, callCtx) 599 return storage.FilesystemInfo{}, errors.Annotate(err, "tagging volume") 600 } 601 } 602 603 return storage.FilesystemInfo{ 604 FilesystemId: filesystemId, 605 Size: size, 606 }, nil 607 }