github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/provider/rootfs.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package provider 5 6 import ( 7 "os" 8 "path/filepath" 9 10 "github.com/juju/errors" 11 "github.com/juju/names/v5" 12 13 "github.com/juju/juju/environs/context" 14 "github.com/juju/juju/storage" 15 ) 16 17 const ( 18 RootfsProviderType = storage.ProviderType("rootfs") 19 ) 20 21 // rootfsProviders create storage sources which provide access to filesystems. 22 type rootfsProvider struct { 23 // run is a function type used for running commands on the local machine. 24 run runCommandFunc 25 } 26 27 var ( 28 _ storage.Provider = (*rootfsProvider)(nil) 29 ) 30 31 func (p *rootfsProvider) ValidateForK8s(attributes map[string]any) error { 32 if attributes == nil { 33 return nil 34 } 35 // check the configuration 36 return checkK8sConfig(attributes) 37 } 38 39 // ValidateConfig is defined on the Provider interface. 40 func (p *rootfsProvider) ValidateConfig(cfg *storage.Config) error { 41 // Rootfs provider has no configuration. 42 return nil 43 } 44 45 // validateFullConfig validates a fully-constructed storage config, 46 // combining the user-specified config and any internally specified 47 // config. 48 func (p *rootfsProvider) validateFullConfig(cfg *storage.Config) error { 49 if err := p.ValidateConfig(cfg); err != nil { 50 return err 51 } 52 storageDir, ok := cfg.ValueString(storage.ConfigStorageDir) 53 if !ok || storageDir == "" { 54 return errors.New("storage directory not specified") 55 } 56 return nil 57 } 58 59 // VolumeSource is defined on the Provider interface. 60 func (p *rootfsProvider) VolumeSource(providerConfig *storage.Config) (storage.VolumeSource, error) { 61 return nil, errors.NotSupportedf("volumes") 62 } 63 64 // FilesystemSource is defined on the Provider interface. 65 func (p *rootfsProvider) FilesystemSource(sourceConfig *storage.Config) (storage.FilesystemSource, error) { 66 if err := p.validateFullConfig(sourceConfig); err != nil { 67 return nil, err 68 } 69 // storageDir is validated by validateFullConfig. 70 storageDir, _ := sourceConfig.ValueString(storage.ConfigStorageDir) 71 return &rootfsFilesystemSource{ 72 &osDirFuncs{run: p.run}, 73 p.run, 74 storageDir, 75 }, nil 76 } 77 78 // Supports is defined on the Provider interface. 79 func (*rootfsProvider) Supports(k storage.StorageKind) bool { 80 return k == storage.StorageKindFilesystem 81 } 82 83 // Scope is defined on the Provider interface. 84 func (*rootfsProvider) Scope() storage.Scope { 85 return storage.ScopeMachine 86 } 87 88 // Dynamic is defined on the Provider interface. 89 func (*rootfsProvider) Dynamic() bool { 90 return true 91 } 92 93 // Releasable is defined on the Provider interface. 94 func (*rootfsProvider) Releasable() bool { 95 return false 96 } 97 98 // DefaultPools is defined on the Provider interface. 99 func (*rootfsProvider) DefaultPools() []*storage.Config { 100 return nil 101 } 102 103 type rootfsFilesystemSource struct { 104 dirFuncs dirFuncs 105 run runCommandFunc 106 storageDir string 107 } 108 109 // ensureDir ensures the specified path is a directory, or 110 // if it does not exist, that a directory can be created there. 111 func ensureDir(d dirFuncs, path string) error { 112 // If path already exists, we check that it is empty. 113 // It is up to the storage provisioner to ensure that any 114 // shared storage constraints and attachments with the same 115 // path are validated etc. So the check here is more a sanity check. 116 fi, err := d.lstat(path) 117 if err == nil { 118 if !fi.IsDir() { 119 return errors.Errorf("path %q must be a directory", path) 120 } 121 return nil 122 } 123 if !os.IsNotExist(err) { 124 return errors.Trace(err) 125 } 126 if err := d.mkDirAll(path, 0755); err != nil { 127 return errors.Annotate(err, "could not create directory") 128 } 129 return nil 130 } 131 132 // ensureEmptyDir ensures the specified directory is empty. 133 func ensureEmptyDir(d dirFuncs, path string) error { 134 fileCount, err := d.fileCount(path) 135 if err != nil { 136 return errors.Annotate(err, "could not read directory") 137 } 138 if fileCount > 0 { 139 return errors.Errorf("%q is not empty", path) 140 } 141 return nil 142 } 143 144 var _ storage.FilesystemSource = (*rootfsFilesystemSource)(nil) 145 146 // ValidateFilesystemParams is defined on the FilesystemSource interface. 147 func (s *rootfsFilesystemSource) ValidateFilesystemParams(params storage.FilesystemParams) error { 148 // ValidateFilesystemParams may be called on a machine other than the 149 // machine where the filesystem will be mounted, so we cannot check 150 // available size until we get to CreateFilesystem. 151 return nil 152 } 153 154 // CreateFilesystems is defined on the FilesystemSource interface. 155 func (s *rootfsFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) { 156 results := make([]storage.CreateFilesystemsResult, len(args)) 157 for i, arg := range args { 158 filesystem, err := s.createFilesystem(arg) 159 if err != nil { 160 results[i].Error = err 161 continue 162 } 163 results[i].Filesystem = filesystem 164 } 165 return results, nil 166 } 167 168 func (s *rootfsFilesystemSource) createFilesystem(params storage.FilesystemParams) (*storage.Filesystem, error) { 169 if err := s.ValidateFilesystemParams(params); err != nil { 170 return nil, errors.Trace(err) 171 } 172 path := filepath.Join(s.storageDir, params.Tag.Id()) 173 if err := ensureDir(s.dirFuncs, path); err != nil { 174 return nil, errors.Trace(err) 175 } 176 if err := ensureEmptyDir(s.dirFuncs, path); err != nil { 177 return nil, errors.Trace(err) 178 } 179 sizeInMiB, err := s.dirFuncs.calculateSize(s.storageDir) 180 if err != nil { 181 os.Remove(path) 182 return nil, errors.Trace(err) 183 } 184 if sizeInMiB < params.Size { 185 os.Remove(path) 186 return nil, errors.Errorf("filesystem is not big enough (%dM < %dM)", sizeInMiB, params.Size) 187 } 188 return &storage.Filesystem{ 189 params.Tag, 190 names.VolumeTag{}, 191 storage.FilesystemInfo{ 192 FilesystemId: params.Tag.Id(), 193 Size: sizeInMiB, 194 }, 195 }, nil 196 } 197 198 // DestroyFilesystems is defined on the FilesystemSource interface. 199 func (s *rootfsFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 200 // DestroyFilesystems is a no-op; we leave the storage directory 201 // in tact for post-mortems and such. 202 return make([]error, len(filesystemIds)), nil 203 } 204 205 // ReleaseFilesystems is defined on the FilesystemSource interface. 206 func (s *rootfsFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 207 return make([]error, len(filesystemIds)), nil 208 } 209 210 // AttachFilesystems is defined on the FilesystemSource interface. 211 func (s *rootfsFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) { 212 results := make([]storage.AttachFilesystemsResult, len(args)) 213 for i, arg := range args { 214 attachment, err := s.attachFilesystem(arg) 215 if err != nil { 216 results[i].Error = err 217 continue 218 } 219 results[i].FilesystemAttachment = attachment 220 } 221 return results, nil 222 } 223 224 func (s *rootfsFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) { 225 mountPoint := arg.Path 226 if mountPoint == "" { 227 return nil, errNoMountPoint 228 } 229 // The filesystem is created at <storage-dir>/<storage-id>. 230 // If it is different to the attachment path, bind mount. 231 if err := s.mount(arg.Filesystem, mountPoint); err != nil { 232 return nil, err 233 } 234 return &storage.FilesystemAttachment{ 235 arg.Filesystem, 236 arg.Machine, 237 storage.FilesystemAttachmentInfo{ 238 Path: mountPoint, 239 }, 240 }, nil 241 } 242 243 func (s *rootfsFilesystemSource) mount(tag names.FilesystemTag, target string) error { 244 fsPath := filepath.Join(s.storageDir, tag.Id()) 245 if target == fsPath { 246 return nil 247 } 248 logger.Debugf("mounting filesystem %q at %q", fsPath, target) 249 250 if err := ensureDir(s.dirFuncs, target); err != nil { 251 return errors.Trace(err) 252 } 253 254 mounted, err := s.tryBindMount(fsPath, target) 255 if err != nil { 256 return errors.Trace(err) 257 } 258 if mounted { 259 return nil 260 } 261 // We couldn't bind-mount over the designated directory; 262 // carry on and check if it's on the same filesystem. If 263 // it is, and it's empty, then claim it as our own. 264 265 if err := s.validateSameMountPoints(fsPath, target); err != nil { 266 return err 267 } 268 269 // The first time we try to take the existing directory, we'll 270 // ensure that it's empty and create a file to "claim" it. 271 // Future attachments will simply ensure that the claim file 272 // exists. 273 targetClaimPath := filepath.Join(fsPath, "juju-target-claimed") 274 _, err = s.dirFuncs.lstat(targetClaimPath) 275 if err == nil { 276 return nil 277 } else if !os.IsNotExist(err) { 278 return errors.Trace(err) 279 } 280 if err := ensureEmptyDir(s.dirFuncs, target); err != nil { 281 return errors.Trace(err) 282 } 283 if err := s.dirFuncs.mkDirAll(targetClaimPath, 0755); err != nil { 284 return errors.Annotate(err, "writing claim file") 285 } 286 return nil 287 } 288 289 func (s *rootfsFilesystemSource) tryBindMount(source, target string) (bool, error) { 290 targetSource, err := s.dirFuncs.mountPointSource(target) 291 if err != nil { 292 return false, errors.Annotate(err, "getting target mount-point source") 293 } 294 if targetSource == source { 295 // Already bind mounted. 296 return true, nil 297 } 298 if err := s.dirFuncs.bindMount(source, target); err != nil { 299 logger.Debugf("cannot bind-mount: %v", err) 300 } else { 301 return true, nil 302 } 303 return false, nil 304 } 305 306 func (s *rootfsFilesystemSource) validateSameMountPoints(source, target string) error { 307 sourceMountPoint, err := s.dirFuncs.mountPoint(source) 308 if err != nil { 309 return errors.Trace(err) 310 } 311 targetMountPoint, err := s.dirFuncs.mountPoint(target) 312 if err != nil { 313 return errors.Trace(err) 314 } 315 if sourceMountPoint != targetMountPoint { 316 return errors.Errorf( 317 "%q (%q) and %q (%q) are on different filesystems", 318 source, sourceMountPoint, target, targetMountPoint, 319 ) 320 } 321 return nil 322 } 323 324 // DetachFilesystems is defined on the FilesystemSource interface. 325 func (s *rootfsFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) { 326 results := make([]error, len(args)) 327 for i, arg := range args { 328 if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil { 329 results[i] = err 330 } 331 } 332 return results, nil 333 }