github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/storage/provider/managedfs.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 "bufio" 8 "fmt" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 "unicode" 14 15 "github.com/juju/collections/set" 16 "github.com/juju/errors" 17 "github.com/juju/names/v5" 18 19 "github.com/juju/juju/environs/context" 20 "github.com/juju/juju/storage" 21 ) 22 23 const ( 24 // defaultFilesystemType is the default filesystem type 25 // to create for volume-backed managed filesystems. 26 defaultFilesystemType = "ext4" 27 ) 28 29 // managedFilesystemSource is an implementation of storage.FilesystemSource 30 // that manages filesystems on volumes attached to the host machine. 31 // 32 // managedFilesystemSource is expected to be called from a single goroutine. 33 type managedFilesystemSource struct { 34 run runCommandFunc 35 dirFuncs dirFuncs 36 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice 37 filesystems map[names.FilesystemTag]storage.Filesystem 38 } 39 40 // NewManagedFilesystemSource returns a storage.FilesystemSource that manages 41 // filesystems on block devices on the host machine. 42 // 43 // The parameters are maps that the caller will update with information about 44 // block devices and filesystems created by the source. The caller must not 45 // update the maps during calls to the source's methods. 46 func NewManagedFilesystemSource( 47 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice, 48 filesystems map[names.FilesystemTag]storage.Filesystem, 49 ) storage.FilesystemSource { 50 return &managedFilesystemSource{ 51 logAndExec, 52 &osDirFuncs{run: logAndExec}, 53 volumeBlockDevices, filesystems, 54 } 55 } 56 57 // ValidateFilesystemParams is defined on storage.FilesystemSource. 58 func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error { 59 // NOTE(axw) the parameters may be for destroying a filesystem, which 60 // may be called when the backing volume is detached from the machine. 61 // We must not perform any validation here that would fail if the 62 // volume is detached. 63 return nil 64 } 65 66 func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) { 67 blockDevice, ok := s.volumeBlockDevices[v] 68 if !ok { 69 return storage.BlockDevice{}, errors.Errorf( 70 "backing-volume %s is not yet attached", v.Id(), 71 ) 72 } 73 return blockDevice, nil 74 } 75 76 // CreateFilesystems is defined on storage.FilesystemSource. 77 func (s *managedFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) { 78 results := make([]storage.CreateFilesystemsResult, len(args)) 79 for i, arg := range args { 80 filesystem, err := s.createFilesystem(arg) 81 if err != nil { 82 results[i].Error = err 83 continue 84 } 85 results[i].Filesystem = filesystem 86 } 87 return results, nil 88 } 89 90 func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) { 91 blockDevice, err := s.backingVolumeBlockDevice(arg.Volume) 92 if err != nil { 93 return nil, errors.Trace(err) 94 } 95 devicePath := devicePath(blockDevice) 96 if isDiskDevice(devicePath) { 97 if err := destroyPartitions(s.run, devicePath); err != nil { 98 return nil, errors.Trace(err) 99 } 100 if err := createPartition(s.run, devicePath); err != nil { 101 return nil, errors.Trace(err) 102 } 103 devicePath = partitionDevicePath(devicePath) 104 } 105 if err := createFilesystem(s.run, devicePath); err != nil { 106 return nil, errors.Trace(err) 107 } 108 return &storage.Filesystem{ 109 arg.Tag, 110 arg.Volume, 111 storage.FilesystemInfo{ 112 arg.Tag.String(), 113 blockDevice.Size, 114 }, 115 }, nil 116 } 117 118 // DestroyFilesystems is defined on storage.FilesystemSource. 119 func (s *managedFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 120 // DestroyFilesystems is a no-op; there is nothing to destroy, 121 // since the filesystem is just data on a volume. The volume 122 // is destroyed separately. 123 return make([]error, len(filesystemIds)), nil 124 } 125 126 // ReleaseFilesystems is defined on storage.FilesystemSource. 127 func (s *managedFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 128 return make([]error, len(filesystemIds)), nil 129 } 130 131 // AttachFilesystems is defined on storage.FilesystemSource. 132 func (s *managedFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) { 133 results := make([]storage.AttachFilesystemsResult, len(args)) 134 for i, arg := range args { 135 attachment, err := s.attachFilesystem(arg) 136 if err != nil { 137 results[i].Error = err 138 continue 139 } 140 results[i].FilesystemAttachment = attachment 141 } 142 return results, nil 143 } 144 145 func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) { 146 filesystem, ok := s.filesystems[arg.Filesystem] 147 if !ok { 148 return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id()) 149 } 150 blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume) 151 if err != nil { 152 return nil, errors.Trace(err) 153 } 154 devicePath := devicePath(blockDevice) 155 if isDiskDevice(devicePath) { 156 devicePath = partitionDevicePath(devicePath) 157 } 158 if err := mountFilesystem(s.run, s.dirFuncs, devicePath, blockDevice.UUID, arg.Path, arg.ReadOnly); err != nil { 159 return nil, errors.Trace(err) 160 } 161 return &storage.FilesystemAttachment{ 162 arg.Filesystem, 163 arg.Machine, 164 storage.FilesystemAttachmentInfo{ 165 arg.Path, 166 arg.ReadOnly, 167 }, 168 }, nil 169 } 170 171 // DetachFilesystems is defined on storage.FilesystemSource. 172 func (s *managedFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) { 173 results := make([]error, len(args)) 174 for i, arg := range args { 175 if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil { 176 results[i] = err 177 } 178 } 179 return results, nil 180 } 181 182 func destroyPartitions(run runCommandFunc, devicePath string) error { 183 logger.Debugf("destroying partitions on %q", devicePath) 184 if _, err := run("sgdisk", "--zap-all", devicePath); err != nil { 185 return errors.Annotate(err, "sgdisk failed") 186 } 187 return nil 188 } 189 190 // createPartition creates a single partition (1) on the disk with the 191 // specified device path. 192 func createPartition(run runCommandFunc, devicePath string) error { 193 logger.Debugf("creating partition on %q", devicePath) 194 if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil { 195 return errors.Annotate(err, "sgdisk failed") 196 } 197 return nil 198 } 199 200 func createFilesystem(run runCommandFunc, devicePath string) error { 201 logger.Debugf("attempting to create filesystem on %q", devicePath) 202 mkfscmd := "mkfs." + defaultFilesystemType 203 _, err := run(mkfscmd, devicePath) 204 if err != nil { 205 return errors.Annotatef(err, "%s failed", mkfscmd) 206 } 207 logger.Infof("created filesystem on %q", devicePath) 208 return nil 209 } 210 211 func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, UUID, mountPoint string, readOnly bool) error { 212 logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint) 213 if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil { 214 return errors.Annotate(err, "creating mount point") 215 } 216 mounted, mountSource, err := isMounted(dirFuncs, mountPoint) 217 if err != nil { 218 return errors.Trace(err) 219 } 220 if mounted { 221 logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint) 222 } else { 223 var args []string 224 if readOnly { 225 args = append(args, "-o", "ro") 226 } 227 args = append(args, devicePath, mountPoint) 228 if _, err := run("mount", args...); err != nil { 229 return errors.Annotate(err, "mount failed") 230 } 231 logger.Debugf("mounted filesystem on %q at %q", devicePath, mountPoint) 232 } 233 // Look for the mtab entry resulting from the mount and copy it to fstab. 234 // This ensures the mount is available available after a reboot. 235 etcDir := dirFuncs.etcDir() 236 mtabEntry, err := extractMtabEntry(etcDir, devicePath, mountPoint) 237 if err != nil { 238 return errors.Annotate(err, "parsing /etc/mtab") 239 } 240 if mtabEntry == "" { 241 return nil 242 } 243 return ensureFstabEntry(etcDir, devicePath, UUID, mountPoint, mtabEntry) 244 } 245 246 // extractMtabEntry returns any /etc/mtab entry for the specified 247 // device path and mount point, or "" if none exists. 248 func extractMtabEntry(etcDir string, devicePath, mountPoint string) (string, error) { 249 f, err := os.Open(filepath.Join(etcDir, "mtab")) 250 if os.IsNotExist(err) { 251 return "", nil 252 } 253 if err != nil { 254 return "", errors.Trace(err) 255 } 256 defer f.Close() 257 scanner := bufio.NewScanner(f) 258 259 for scanner.Scan() { 260 line := scanner.Text() 261 fields := strings.Fields(line) 262 if len(fields) >= 2 && fields[0] == devicePath && fields[1] == mountPoint { 263 return line, nil 264 } 265 } 266 267 if err := scanner.Err(); err != nil { 268 return "", errors.Trace(err) 269 } 270 return "", nil 271 } 272 273 // ensureFstabEntry creates an entry in /etc/fstab for the specified 274 // device path and mount point so long as there's no existing entry already. 275 func ensureFstabEntry(etcDir, devicePath, UUID, mountPoint, entry string) error { 276 f, err := os.Open(filepath.Join(etcDir, "fstab")) 277 if err != nil && !os.IsNotExist(err) { 278 return errors.Annotate(err, "opening /etc/fstab") 279 } 280 if err == nil { 281 defer f.Close() 282 } 283 284 newFsTab, err := os.CreateTemp(etcDir, "juju-fstab-") 285 if err != nil { 286 return errors.Trace(err) 287 } 288 defer func() { 289 _ = newFsTab.Close() 290 _ = os.Remove(newFsTab.Name()) 291 }() 292 if err := os.Chmod(newFsTab.Name(), 0644); err != nil { 293 return errors.Trace(err) 294 } 295 296 // Add nofail if not there already 297 resultFields := strings.Fields(entry) 298 options := set.NewStrings() 299 if len(resultFields) >= 4 { 300 options = set.NewStrings(strings.Split(resultFields[3], ",")...) 301 } 302 if !options.Contains("nofail") { 303 options.Add("nofail") 304 opts := strings.Join(options.SortedValues(), ",") 305 if len(resultFields) >= 4 { 306 resultFields[3] = opts 307 } else { 308 resultFields = append(resultFields, opts) 309 } 310 } 311 312 uuidField := "UUID=" + UUID 313 addNewEntry := true 314 // Scan all the fstab lines, searching for one 315 // which describes the entry we want to create. 316 scanner := bufio.NewScanner(f) 317 for f != nil && scanner.Scan() { 318 line := scanner.Text() 319 fields := strings.Fields(line) 320 if len(fields) < 2 || fields[1] != mountPoint { 321 goto writeLine 322 } 323 // Is the line the UUID based mount entry we want. 324 if fields[0] == uuidField { 325 addNewEntry = false 326 goto writeLine 327 } 328 // Is the line for some other entry. 329 if fields[0] != devicePath { 330 goto writeLine 331 } 332 // We have a match, if UUID is not yet known, retain the line. 333 if UUID == "" { 334 addNewEntry = false 335 goto writeLine 336 } 337 continue 338 writeLine: 339 _, err := newFsTab.WriteString(line + "\n") 340 if err != nil { 341 return errors.Trace(err) 342 } 343 } 344 if err := scanner.Err(); err != nil { 345 return errors.Trace(err) 346 } 347 348 if addNewEntry { 349 if UUID != "" { 350 if len(resultFields) >= 2 { // just being defensive, check should never fail. 351 _, err := newFsTab.WriteString(fmt.Sprintf("# %s was on %s during installation\n", resultFields[1], resultFields[0])) 352 if err != nil { 353 return errors.Trace(err) 354 } 355 } 356 resultFields[0] = uuidField 357 } 358 _, err := newFsTab.WriteString(strings.Join(resultFields, " ") + "\n") 359 if err != nil { 360 return errors.Trace(err) 361 } 362 363 } 364 return os.Rename(newFsTab.Name(), filepath.Join(etcDir, "fstab")) 365 } 366 367 func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error { 368 mounted, _, err := isMounted(dirFuncs, mountPoint) 369 if err != nil { 370 return errors.Trace(err) 371 } 372 if !mounted { 373 return nil 374 } 375 logger.Debugf("attempting to unmount filesystem at %q", mountPoint) 376 if err := removeFstabEntry(dirFuncs.etcDir(), mountPoint); err != nil { 377 return errors.Annotate(err, "updating /etc/fstab failed") 378 } 379 if _, err := run("umount", mountPoint); err != nil { 380 return errors.Annotate(err, "umount failed") 381 } 382 logger.Infof("unmounted filesystem at %q", mountPoint) 383 return nil 384 } 385 386 // removeFstabEntry removes any existing /etc/fstab entry for 387 // the specified mount point. 388 func removeFstabEntry(etcDir string, mountPoint string) error { 389 fstab := filepath.Join(etcDir, "fstab") 390 f, err := os.Open(fstab) 391 if os.IsNotExist(err) { 392 return nil 393 } 394 if err != nil { 395 return errors.Trace(err) 396 } 397 defer f.Close() 398 scanner := bufio.NewScanner(f) 399 400 // Use a tempfile in /etc and rename when done. 401 newFsTab, err := os.CreateTemp(etcDir, "juju-fstab-") 402 if err != nil { 403 return errors.Trace(err) 404 } 405 defer func() { 406 _ = newFsTab.Close() 407 _ = os.Remove(newFsTab.Name()) 408 }() 409 if err := os.Chmod(newFsTab.Name(), 0644); err != nil { 410 return errors.Trace(err) 411 } 412 413 for scanner.Scan() { 414 line := scanner.Text() 415 fields := strings.Fields(line) 416 if len(fields) < 2 || fields[1] != mountPoint { 417 _, err := newFsTab.WriteString(line + "\n") 418 if err != nil { 419 return errors.Trace(err) 420 } 421 } 422 } 423 if err := scanner.Err(); err != nil { 424 return errors.Trace(err) 425 } 426 427 return os.Rename(newFsTab.Name(), fstab) 428 } 429 430 func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) { 431 mountPointParent := filepath.Dir(mountPoint) 432 parentSource, err := dirFuncs.mountPointSource(mountPointParent) 433 if err != nil { 434 return false, "", errors.Trace(err) 435 } 436 source, err := dirFuncs.mountPointSource(mountPoint) 437 if err != nil { 438 return false, "", errors.Trace(err) 439 } 440 if source != parentSource { 441 // Already mounted. 442 return true, source, nil 443 } 444 return false, "", nil 445 } 446 447 // devicePath returns the device path for the given block device. 448 func devicePath(dev storage.BlockDevice) string { 449 return path.Join("/dev", dev.DeviceName) 450 } 451 452 // partitionDevicePath returns the device path for the first (and only) 453 // partition of the disk with the specified device path. 454 func partitionDevicePath(devicePath string) string { 455 return devicePath + "1" 456 } 457 458 // isDiskDevice reports whether or not the device is a full disk, as opposed 459 // to a partition or a loop device. We create a partition on disks to contain 460 // filesystems. 461 func isDiskDevice(devicePath string) bool { 462 var last rune 463 for _, r := range devicePath { 464 last = r 465 } 466 return !unicode.IsDigit(last) 467 }