github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 12 "strings" 13 "unicode" 14 15 "github.com/juju/errors" 16 "gopkg.in/juju/names.v2" 17 18 "github.com/juju/juju/environs/context" 19 "github.com/juju/juju/storage" 20 ) 21 22 const ( 23 // defaultFilesystemType is the default filesystem type 24 // to create for volume-backed managed filesystems. 25 defaultFilesystemType = "ext4" 26 ) 27 28 // managedFilesystemSource is an implementation of storage.FilesystemSource 29 // that manages filesystems on volumes attached to the host machine. 30 // 31 // managedFilesystemSource is expected to be called from a single goroutine. 32 type managedFilesystemSource struct { 33 run runCommandFunc 34 dirFuncs dirFuncs 35 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice 36 filesystems map[names.FilesystemTag]storage.Filesystem 37 } 38 39 // NewManagedFilesystemSource returns a storage.FilesystemSource that manages 40 // filesystems on block devices on the host machine. 41 // 42 // The parameters are maps that the caller will update with information about 43 // block devices and filesystems created by the source. The caller must not 44 // update the maps during calls to the source's methods. 45 func NewManagedFilesystemSource( 46 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice, 47 filesystems map[names.FilesystemTag]storage.Filesystem, 48 ) storage.FilesystemSource { 49 return &managedFilesystemSource{ 50 logAndExec, 51 &osDirFuncs{logAndExec}, 52 volumeBlockDevices, filesystems, 53 } 54 } 55 56 // ValidateFilesystemParams is defined on storage.FilesystemSource. 57 func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error { 58 // NOTE(axw) the parameters may be for destroying a filesystem, which 59 // may be called when the backing volume is detached from the machine. 60 // We must not perform any validation here that would fail if the 61 // volume is detached. 62 return nil 63 } 64 65 func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) { 66 blockDevice, ok := s.volumeBlockDevices[v] 67 if !ok { 68 return storage.BlockDevice{}, errors.Errorf( 69 "backing-volume %s is not yet attached", v.Id(), 70 ) 71 } 72 return blockDevice, nil 73 } 74 75 // CreateFilesystems is defined on storage.FilesystemSource. 76 func (s *managedFilesystemSource) CreateFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) { 77 results := make([]storage.CreateFilesystemsResult, len(args)) 78 for i, arg := range args { 79 filesystem, err := s.createFilesystem(arg) 80 if err != nil { 81 results[i].Error = err 82 continue 83 } 84 results[i].Filesystem = filesystem 85 } 86 return results, nil 87 } 88 89 func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) { 90 blockDevice, err := s.backingVolumeBlockDevice(arg.Volume) 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 devicePath := devicePath(blockDevice) 95 if isDiskDevice(devicePath) { 96 if err := destroyPartitions(s.run, devicePath); err != nil { 97 return nil, errors.Trace(err) 98 } 99 if err := createPartition(s.run, devicePath); err != nil { 100 return nil, errors.Trace(err) 101 } 102 devicePath = partitionDevicePath(devicePath) 103 } 104 if err := createFilesystem(s.run, devicePath); err != nil { 105 return nil, errors.Trace(err) 106 } 107 return &storage.Filesystem{ 108 arg.Tag, 109 arg.Volume, 110 storage.FilesystemInfo{ 111 arg.Tag.String(), 112 blockDevice.Size, 113 }, 114 }, nil 115 } 116 117 // DestroyFilesystems is defined on storage.FilesystemSource. 118 func (s *managedFilesystemSource) DestroyFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 119 // DestroyFilesystems is a no-op; there is nothing to destroy, 120 // since the filesystem is just data on a volume. The volume 121 // is destroyed separately. 122 return make([]error, len(filesystemIds)), nil 123 } 124 125 // ReleaseFilesystems is defined on storage.FilesystemSource. 126 func (s *managedFilesystemSource) ReleaseFilesystems(ctx context.ProviderCallContext, filesystemIds []string) ([]error, error) { 127 return make([]error, len(filesystemIds)), nil 128 } 129 130 // AttachFilesystems is defined on storage.FilesystemSource. 131 func (s *managedFilesystemSource) AttachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) { 132 results := make([]storage.AttachFilesystemsResult, len(args)) 133 for i, arg := range args { 134 attachment, err := s.attachFilesystem(arg) 135 if err != nil { 136 results[i].Error = err 137 continue 138 } 139 results[i].FilesystemAttachment = attachment 140 } 141 return results, nil 142 } 143 144 func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) { 145 filesystem, ok := s.filesystems[arg.Filesystem] 146 if !ok { 147 return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id()) 148 } 149 blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume) 150 if err != nil { 151 return nil, errors.Trace(err) 152 } 153 devicePath := devicePath(blockDevice) 154 if isDiskDevice(devicePath) { 155 devicePath = partitionDevicePath(devicePath) 156 } 157 if err := mountFilesystem(s.run, s.dirFuncs, devicePath, arg.Path, arg.ReadOnly); err != nil { 158 return nil, errors.Trace(err) 159 } 160 return &storage.FilesystemAttachment{ 161 arg.Filesystem, 162 arg.Machine, 163 storage.FilesystemAttachmentInfo{ 164 arg.Path, 165 arg.ReadOnly, 166 }, 167 }, nil 168 } 169 170 // DetachFilesystems is defined on storage.FilesystemSource. 171 func (s *managedFilesystemSource) DetachFilesystems(ctx context.ProviderCallContext, args []storage.FilesystemAttachmentParams) ([]error, error) { 172 results := make([]error, len(args)) 173 for i, arg := range args { 174 if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil { 175 results[i] = err 176 } 177 } 178 return results, nil 179 } 180 181 func destroyPartitions(run runCommandFunc, devicePath string) error { 182 logger.Debugf("destroying partitions on %q", devicePath) 183 if _, err := run("sgdisk", "--zap-all", devicePath); err != nil { 184 return errors.Annotate(err, "sgdisk failed") 185 } 186 return nil 187 } 188 189 // createPartition creates a single partition (1) on the disk with the 190 // specified device path. 191 func createPartition(run runCommandFunc, devicePath string) error { 192 logger.Debugf("creating partition on %q", devicePath) 193 if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil { 194 return errors.Annotate(err, "sgdisk failed") 195 } 196 return nil 197 } 198 199 func createFilesystem(run runCommandFunc, devicePath string) error { 200 logger.Debugf("attempting to create filesystem on %q", devicePath) 201 mkfscmd := "mkfs." + defaultFilesystemType 202 _, err := run(mkfscmd, devicePath) 203 if err != nil { 204 return errors.Annotatef(err, "%s failed", mkfscmd) 205 } 206 logger.Infof("created filesystem on %q", devicePath) 207 return nil 208 } 209 210 func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, mountPoint string, readOnly bool) error { 211 logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint) 212 if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil { 213 return errors.Annotate(err, "creating mount point") 214 } 215 mounted, mountSource, err := isMounted(dirFuncs, mountPoint) 216 if err != nil { 217 return errors.Trace(err) 218 } 219 if mounted { 220 logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint) 221 return nil 222 } 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.Infof("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 addFstabEntry(etcDir, devicePath, 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 // addFstabEntry 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 addFstabEntry(etcDir string, devicePath, mountPoint, entry string) error { 276 f, err := os.OpenFile(filepath.Join(etcDir, "fstab"), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) 277 if err != nil { 278 return errors.Annotate(err, "opening /etc/fstab") 279 } 280 defer f.Close() 281 282 // Ensure there's no entry there already 283 scanner := bufio.NewScanner(f) 284 for scanner.Scan() { 285 line := scanner.Text() 286 fields := strings.Fields(line) 287 if len(fields) >= 2 && fields[0] == devicePath && fields[1] == mountPoint { 288 return nil 289 } 290 } 291 if err := scanner.Err(); err != nil { 292 return errors.Trace(err) 293 } 294 295 // The entry will be written at the end of the fstab file. 296 if _, err = f.WriteString("\n" + entry + "\n"); err != nil { 297 return errors.Annotate(err, "writing /etc/fstab") 298 } 299 return nil 300 } 301 302 func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error { 303 mounted, _, err := isMounted(dirFuncs, mountPoint) 304 if err != nil { 305 return errors.Trace(err) 306 } 307 if !mounted { 308 return nil 309 } 310 logger.Debugf("attempting to unmount filesystem at %q", mountPoint) 311 if err := removeFstabEntry(dirFuncs.etcDir(), mountPoint); err != nil { 312 return errors.Annotate(err, "updating /etc/fstab failed") 313 } 314 if _, err := run("umount", mountPoint); err != nil { 315 return errors.Annotate(err, "umount failed") 316 } 317 logger.Infof("unmounted filesystem at %q", mountPoint) 318 return nil 319 } 320 321 // removeFstabEntry removes any existing /etc/fstab entry for 322 // the specified mount point. 323 func removeFstabEntry(etcDir string, mountPoint string) error { 324 fstab := filepath.Join(etcDir, "fstab") 325 f, err := os.Open(fstab) 326 if os.IsNotExist(err) { 327 return nil 328 } 329 if err != nil { 330 return errors.Trace(err) 331 } 332 defer f.Close() 333 scanner := bufio.NewScanner(f) 334 335 // Use a tempfile in /etc and rename when done. 336 newFsTab, err := ioutil.TempFile(etcDir, "juju-fstab-") 337 if err != nil { 338 return errors.Trace(err) 339 } 340 defer func() { 341 newFsTab.Close() 342 os.Remove(newFsTab.Name()) 343 }() 344 if err := os.Chmod(newFsTab.Name(), 0644); err != nil { 345 return errors.Trace(err) 346 } 347 348 for scanner.Scan() { 349 line := scanner.Text() 350 fields := strings.Fields(line) 351 if len(fields) < 2 || fields[1] != mountPoint { 352 _, err := newFsTab.WriteString(line + "\n") 353 if err != nil { 354 return errors.Trace(err) 355 } 356 } 357 } 358 if err := scanner.Err(); err != nil { 359 return errors.Trace(err) 360 } 361 362 return os.Rename(newFsTab.Name(), fstab) 363 } 364 365 func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) { 366 mountPointParent := filepath.Dir(mountPoint) 367 parentSource, err := dirFuncs.mountPointSource(mountPointParent) 368 if err != nil { 369 return false, "", errors.Trace(err) 370 } 371 source, err := dirFuncs.mountPointSource(mountPoint) 372 if err != nil { 373 return false, "", errors.Trace(err) 374 } 375 if source != parentSource { 376 // Already mounted. 377 return true, source, nil 378 } 379 return false, "", nil 380 } 381 382 // devicePath returns the device path for the given block device. 383 func devicePath(dev storage.BlockDevice) string { 384 return path.Join("/dev", dev.DeviceName) 385 } 386 387 // partitionDevicePath returns the device path for the first (and only) 388 // partition of the disk with the specified device path. 389 func partitionDevicePath(devicePath string) string { 390 return devicePath + "1" 391 } 392 393 // isDiskDevice reports whether or not the device is a full disk, as opposed 394 // to a partition or a loop device. We create a partition on disks to contain 395 // filesystems. 396 func isDiskDevice(devicePath string) bool { 397 var last rune 398 for _, r := range devicePath { 399 last = r 400 } 401 return !unicode.IsDigit(last) 402 }