github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/provider/loop.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 "fmt" 8 "os" 9 "path" 10 "path/filepath" 11 "strings" 12 13 "github.com/juju/errors" 14 "github.com/juju/names/v5" 15 16 "github.com/juju/juju/environs/context" 17 "github.com/juju/juju/storage" 18 ) 19 20 const ( 21 // Loop provider types. 22 LoopProviderType = storage.ProviderType("loop") 23 HostLoopProviderType = storage.ProviderType("hostloop") 24 ) 25 26 // loopProviders create volume sources which use loop devices. 27 type loopProvider struct { 28 // run is a function used for running commands on the local machine. 29 run runCommandFunc 30 } 31 32 var _ storage.Provider = (*loopProvider)(nil) 33 34 func (*loopProvider) ValidateForK8s(map[string]any) error { 35 return errors.NotValidf("storage provider type %q", LoopProviderType) 36 } 37 38 // ValidateConfig is defined on the Provider interface. 39 func (*loopProvider) ValidateConfig(*storage.Config) error { 40 // Loop provider has no configuration. 41 return nil 42 } 43 44 // validateFullConfig validates a fully-constructed storage config, 45 // combining the user-specified config and any internally specified 46 // config. 47 func (lp *loopProvider) validateFullConfig(cfg *storage.Config) error { 48 if err := lp.ValidateConfig(cfg); err != nil { 49 return err 50 } 51 storageDir, ok := cfg.ValueString(storage.ConfigStorageDir) 52 if !ok || storageDir == "" { 53 return errors.New("storage directory not specified") 54 } 55 return nil 56 } 57 58 // VolumeSource is defined on the Provider interface. 59 func (lp *loopProvider) VolumeSource(sourceConfig *storage.Config) (storage.VolumeSource, error) { 60 if err := lp.validateFullConfig(sourceConfig); err != nil { 61 return nil, err 62 } 63 // storageDir is validated by validateFullConfig. 64 storageDir, _ := sourceConfig.ValueString(storage.ConfigStorageDir) 65 return &loopVolumeSource{ 66 &osDirFuncs{run: lp.run}, 67 lp.run, 68 storageDir, 69 }, nil 70 } 71 72 // FilesystemSource is defined on the Provider interface. 73 func (lp *loopProvider) FilesystemSource(providerConfig *storage.Config) (storage.FilesystemSource, error) { 74 return nil, errors.NotSupportedf("filesystems") 75 } 76 77 // Supports is defined on the Provider interface. 78 func (*loopProvider) Supports(k storage.StorageKind) bool { 79 return k == storage.StorageKindBlock 80 } 81 82 // Scope is defined on the Provider interface. 83 func (*loopProvider) Scope() storage.Scope { 84 return storage.ScopeMachine 85 } 86 87 // Dynamic is defined on the Provider interface. 88 func (*loopProvider) Dynamic() bool { 89 return true 90 } 91 92 // Releasable is defined on the Provider interface. 93 func (*loopProvider) Releasable() bool { 94 return false 95 } 96 97 // DefaultPools is defined on the Provider interface. 98 func (*loopProvider) DefaultPools() []*storage.Config { 99 return nil 100 } 101 102 // loopVolumeSource provides common functionality to handle 103 // loop devices for rootfs and host loop volume sources. 104 type loopVolumeSource struct { 105 dirFuncs dirFuncs 106 run runCommandFunc 107 storageDir string 108 } 109 110 var _ storage.VolumeSource = (*loopVolumeSource)(nil) 111 112 // CreateVolumes is defined on the VolumeSource interface. 113 func (lvs *loopVolumeSource) CreateVolumes(ctx context.ProviderCallContext, args []storage.VolumeParams) ([]storage.CreateVolumesResult, error) { 114 results := make([]storage.CreateVolumesResult, len(args)) 115 for i, arg := range args { 116 volume, err := lvs.createVolume(arg) 117 if err != nil { 118 results[i].Error = errors.Annotate(err, "creating volume") 119 } 120 results[i].Volume = &volume 121 } 122 return results, nil 123 } 124 125 func (lvs *loopVolumeSource) createVolume(params storage.VolumeParams) (storage.Volume, error) { 126 volumeId := params.Tag.String() 127 loopFilePath := lvs.volumeFilePath(params.Tag) 128 if err := ensureDir(lvs.dirFuncs, filepath.Dir(loopFilePath)); err != nil { 129 return storage.Volume{}, errors.Trace(err) 130 } 131 if err := createBlockFile(lvs.run, loopFilePath, params.Size); err != nil { 132 return storage.Volume{}, errors.Annotate(err, "could not create block file") 133 } 134 return storage.Volume{ 135 params.Tag, 136 storage.VolumeInfo{ 137 VolumeId: volumeId, 138 Size: params.Size, 139 }, 140 }, nil 141 } 142 143 func (lvs *loopVolumeSource) volumeFilePath(tag names.VolumeTag) string { 144 return filepath.Join(lvs.storageDir, tag.String()) 145 } 146 147 // ListVolumes is defined on the VolumeSource interface. 148 func (lvs *loopVolumeSource) ListVolumes(ctx context.ProviderCallContext) ([]string, error) { 149 // TODO(axw) implement this when we need it. 150 return nil, errors.NotImplementedf("ListVolumes") 151 } 152 153 // DescribeVolumes is defined on the VolumeSource interface. 154 func (lvs *loopVolumeSource) DescribeVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]storage.DescribeVolumesResult, error) { 155 // TODO(axw) implement this when we need it. 156 return nil, errors.NotImplementedf("DescribeVolumes") 157 } 158 159 // DestroyVolumes is defined on the VolumeSource interface. 160 func (lvs *loopVolumeSource) DestroyVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 161 results := make([]error, len(volumeIds)) 162 for i, volumeId := range volumeIds { 163 if err := lvs.destroyVolume(volumeId); err != nil { 164 results[i] = errors.Annotatef(err, "destroying %q", volumeId) 165 } 166 } 167 return results, nil 168 } 169 170 // ReleaseVolumes is defined on the VolumeSource interface. 171 func (lvs *loopVolumeSource) ReleaseVolumes(ctx context.ProviderCallContext, volumeIds []string) ([]error, error) { 172 return make([]error, len(volumeIds)), nil 173 } 174 175 func (lvs *loopVolumeSource) destroyVolume(volumeId string) error { 176 tag, err := names.ParseVolumeTag(volumeId) 177 if err != nil { 178 return errors.Errorf("invalid loop volume ID %q", volumeId) 179 } 180 loopFilePath := lvs.volumeFilePath(tag) 181 err = os.Remove(loopFilePath) 182 if err != nil && !os.IsNotExist(err) { 183 return errors.Annotate(err, "removing loop backing file") 184 } 185 return nil 186 } 187 188 // ValidateVolumeParams is defined on the VolumeSource interface. 189 func (lvs *loopVolumeSource) ValidateVolumeParams(params storage.VolumeParams) error { 190 // ValdiateVolumeParams may be called on a machine other than the 191 // machine where the loop device will be created, so we cannot check 192 // available size until we get to CreateVolumes. 193 return nil 194 } 195 196 // AttachVolumes is defined on the VolumeSource interface. 197 func (lvs *loopVolumeSource) AttachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]storage.AttachVolumesResult, error) { 198 results := make([]storage.AttachVolumesResult, len(args)) 199 for i, arg := range args { 200 attachment, err := lvs.attachVolume(arg) 201 if err != nil { 202 results[i].Error = errors.Annotatef(err, "attaching volume %v", arg.Volume.Id()) 203 continue 204 } 205 results[i].VolumeAttachment = attachment 206 } 207 return results, nil 208 } 209 210 func (lvs *loopVolumeSource) attachVolume(arg storage.VolumeAttachmentParams) (*storage.VolumeAttachment, error) { 211 loopFilePath := lvs.volumeFilePath(arg.Volume) 212 deviceName, err := attachLoopDevice(lvs.run, loopFilePath, arg.ReadOnly) 213 if err != nil { 214 os.Remove(loopFilePath) 215 return nil, errors.Annotate(err, "attaching loop device") 216 } 217 return &storage.VolumeAttachment{ 218 arg.Volume, 219 arg.Machine, 220 storage.VolumeAttachmentInfo{ 221 DeviceName: deviceName, 222 ReadOnly: arg.ReadOnly, 223 }, 224 }, nil 225 } 226 227 // DetachVolumes is defined on the VolumeSource interface. 228 func (lvs *loopVolumeSource) DetachVolumes(ctx context.ProviderCallContext, args []storage.VolumeAttachmentParams) ([]error, error) { 229 results := make([]error, len(args)) 230 for i, arg := range args { 231 if err := lvs.detachVolume(arg.Volume); err != nil { 232 results[i] = errors.Annotatef(err, "detaching volume %s", arg.Volume.Id()) 233 } 234 } 235 return results, nil 236 } 237 238 func (lvs *loopVolumeSource) detachVolume(tag names.VolumeTag) error { 239 loopFilePath := lvs.volumeFilePath(tag) 240 deviceNames, err := associatedLoopDevices(lvs.run, loopFilePath) 241 if err != nil { 242 return errors.Annotate(err, "locating loop device") 243 } 244 if len(deviceNames) > 1 { 245 logger.Errorf("expected 1 loop device, got %d", len(deviceNames)) 246 } 247 for _, deviceName := range deviceNames { 248 if err := detachLoopDevice(lvs.run, deviceName); err != nil { 249 return errors.Trace(err) 250 } 251 } 252 return nil 253 } 254 255 // createBlockFile creates a file at the specified path, with the 256 // given size in mebibytes. 257 func createBlockFile(run runCommandFunc, filePath string, sizeInMiB uint64) error { 258 // fallocate will reserve the space without actually writing to it. 259 _, err := run("fallocate", "-l", fmt.Sprintf("%dMiB", sizeInMiB), filePath) 260 if err != nil { 261 return errors.Annotatef(err, "allocating loop backing file %q", filePath) 262 } 263 return nil 264 } 265 266 // attachLoopDevice attaches a loop device to the file with the 267 // specified path, and returns the loop device's name (e.g. "loop0"). 268 // losetup will create additional loop devices as necessary. 269 func attachLoopDevice(run runCommandFunc, filePath string, readOnly bool) (loopDeviceName string, _ error) { 270 devices, err := associatedLoopDevices(run, filePath) 271 if err != nil { 272 return "", err 273 } 274 if len(devices) > 0 { 275 // Already attached. 276 logger.Debugf("%s already attached to %s", filePath, devices) 277 return devices[0], nil 278 } 279 // -f automatically finds the first available loop-device. 280 // -r sets up a read-only loop-device. 281 // --show returns the loop device chosen on stdout. 282 args := []string{"-f", "--show"} 283 if readOnly { 284 args = append(args, "-r") 285 } 286 args = append(args, filePath) 287 stdout, err := run("losetup", args...) 288 if err != nil { 289 return "", errors.Annotatef(err, "attaching loop device to %q", filePath) 290 } 291 stdout = strings.TrimSpace(stdout) 292 loopDeviceName = stdout[len("/dev/"):] 293 return loopDeviceName, nil 294 } 295 296 // detachLoopDevice detaches the loop device with the specified name. 297 func detachLoopDevice(run runCommandFunc, deviceName string) error { 298 _, err := run("losetup", "-d", path.Join("/dev", deviceName)) 299 if err != nil { 300 return errors.Annotatef(err, "detaching loop device %q", deviceName) 301 } 302 return err 303 } 304 305 // associatedLoopDevices returns the device names of the loop devices 306 // associated with the specified file path. 307 func associatedLoopDevices(run runCommandFunc, filePath string) ([]string, error) { 308 stdout, err := run("losetup", "-j", filePath) 309 if err != nil { 310 return nil, errors.Trace(err) 311 } 312 stdout = strings.TrimSpace(stdout) 313 if stdout == "" { 314 return nil, nil 315 } 316 // The output will be zero or more lines with the format: 317 // "/dev/loop0: [0021]:7504142 (/tmp/test.dat)" 318 lines := strings.Split(stdout, "\n") 319 deviceNames := make([]string, len(lines)) 320 for i, line := range lines { 321 pos := strings.IndexRune(line, ':') 322 if pos == -1 { 323 return nil, errors.Errorf("unexpected output %q", line) 324 } 325 deviceName := line[:pos][len("/dev/"):] 326 deviceNames[i] = deviceName 327 } 328 return deviceNames, nil 329 }