github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 "path" 8 "path/filepath" 9 "unicode" 10 11 "github.com/juju/errors" 12 "github.com/juju/names" 13 14 "github.com/juju/juju/storage" 15 ) 16 17 const ( 18 // defaultFilesystemType is the default filesystem type 19 // to create for volume-backed managed filesystems. 20 defaultFilesystemType = "ext4" 21 ) 22 23 // managedFilesystemSource is an implementation of storage.FilesystemSource 24 // that manages filesystems on volumes attached to the host machine. 25 // 26 // managedFilesystemSource is expected to be called from a single goroutine. 27 type managedFilesystemSource struct { 28 run runCommandFunc 29 dirFuncs dirFuncs 30 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice 31 filesystems map[names.FilesystemTag]storage.Filesystem 32 } 33 34 // NewManagedFilesystemSource returns a storage.FilesystemSource that manages 35 // filesystems on block devices on the host machine. 36 // 37 // The parameters are maps that the caller will update with information about 38 // block devices and filesystems created by the source. The caller must not 39 // update the maps during calls to the source's methods. 40 func NewManagedFilesystemSource( 41 volumeBlockDevices map[names.VolumeTag]storage.BlockDevice, 42 filesystems map[names.FilesystemTag]storage.Filesystem, 43 ) storage.FilesystemSource { 44 return &managedFilesystemSource{ 45 logAndExec, 46 &osDirFuncs{logAndExec}, 47 volumeBlockDevices, filesystems, 48 } 49 } 50 51 // ValidateFilesystemParams is defined on storage.FilesystemSource. 52 func (s *managedFilesystemSource) ValidateFilesystemParams(arg storage.FilesystemParams) error { 53 if _, err := s.backingVolumeBlockDevice(arg.Volume); err != nil { 54 return errors.Trace(err) 55 } 56 return nil 57 } 58 59 func (s *managedFilesystemSource) backingVolumeBlockDevice(v names.VolumeTag) (storage.BlockDevice, error) { 60 blockDevice, ok := s.volumeBlockDevices[v] 61 if !ok { 62 return storage.BlockDevice{}, errors.Errorf( 63 "backing-volume %s is not yet attached", v.Id(), 64 ) 65 } 66 return blockDevice, nil 67 } 68 69 // CreateFilesystems is defined on storage.FilesystemSource. 70 func (s *managedFilesystemSource) CreateFilesystems(args []storage.FilesystemParams) ([]storage.CreateFilesystemsResult, error) { 71 results := make([]storage.CreateFilesystemsResult, len(args)) 72 for i, arg := range args { 73 filesystem, err := s.createFilesystem(arg) 74 if err != nil { 75 results[i].Error = err 76 continue 77 } 78 results[i].Filesystem = filesystem 79 } 80 return results, nil 81 } 82 83 func (s *managedFilesystemSource) createFilesystem(arg storage.FilesystemParams) (*storage.Filesystem, error) { 84 blockDevice, err := s.backingVolumeBlockDevice(arg.Volume) 85 if err != nil { 86 return nil, errors.Trace(err) 87 } 88 devicePath := devicePath(blockDevice) 89 if isDiskDevice(devicePath) { 90 if err := destroyPartitions(s.run, devicePath); err != nil { 91 return nil, errors.Trace(err) 92 } 93 if err := createPartition(s.run, devicePath); err != nil { 94 return nil, errors.Trace(err) 95 } 96 devicePath = partitionDevicePath(devicePath) 97 } 98 if err := createFilesystem(s.run, devicePath); err != nil { 99 return nil, errors.Trace(err) 100 } 101 return &storage.Filesystem{ 102 arg.Tag, 103 arg.Volume, 104 storage.FilesystemInfo{ 105 arg.Tag.String(), 106 blockDevice.Size, 107 }, 108 }, nil 109 } 110 111 // DestroyFilesystems is defined on storage.FilesystemSource. 112 func (s *managedFilesystemSource) DestroyFilesystems(filesystemIds []string) ([]error, error) { 113 // DestroyFilesystems is a no-op; there is nothing to destroy, 114 // since the filesystem is just data on a volume. The volume 115 // is destroyed separately. 116 return make([]error, len(filesystemIds)), nil 117 } 118 119 // AttachFilesystems is defined on storage.FilesystemSource. 120 func (s *managedFilesystemSource) AttachFilesystems(args []storage.FilesystemAttachmentParams) ([]storage.AttachFilesystemsResult, error) { 121 results := make([]storage.AttachFilesystemsResult, len(args)) 122 for i, arg := range args { 123 attachment, err := s.attachFilesystem(arg) 124 if err != nil { 125 results[i].Error = err 126 continue 127 } 128 results[i].FilesystemAttachment = attachment 129 } 130 return results, nil 131 } 132 133 func (s *managedFilesystemSource) attachFilesystem(arg storage.FilesystemAttachmentParams) (*storage.FilesystemAttachment, error) { 134 filesystem, ok := s.filesystems[arg.Filesystem] 135 if !ok { 136 return nil, errors.Errorf("filesystem %v is not yet provisioned", arg.Filesystem.Id()) 137 } 138 blockDevice, err := s.backingVolumeBlockDevice(filesystem.Volume) 139 if err != nil { 140 return nil, errors.Trace(err) 141 } 142 devicePath := devicePath(blockDevice) 143 if isDiskDevice(devicePath) { 144 devicePath = partitionDevicePath(devicePath) 145 } 146 if err := mountFilesystem(s.run, s.dirFuncs, devicePath, arg.Path, arg.ReadOnly); err != nil { 147 return nil, errors.Trace(err) 148 } 149 return &storage.FilesystemAttachment{ 150 arg.Filesystem, 151 arg.Machine, 152 storage.FilesystemAttachmentInfo{ 153 arg.Path, 154 arg.ReadOnly, 155 }, 156 }, nil 157 } 158 159 // DetachFilesystems is defined on storage.FilesystemSource. 160 func (s *managedFilesystemSource) DetachFilesystems(args []storage.FilesystemAttachmentParams) ([]error, error) { 161 results := make([]error, len(args)) 162 for i, arg := range args { 163 if err := maybeUnmount(s.run, s.dirFuncs, arg.Path); err != nil { 164 results[i] = err 165 } 166 } 167 return results, nil 168 } 169 170 func destroyPartitions(run runCommandFunc, devicePath string) error { 171 logger.Debugf("destroying partitions on %q", devicePath) 172 if _, err := run("sgdisk", "--zap-all", devicePath); err != nil { 173 return errors.Annotate(err, "sgdisk failed") 174 } 175 return nil 176 } 177 178 // createPartition creates a single partition (1) on the disk with the 179 // specified device path. 180 func createPartition(run runCommandFunc, devicePath string) error { 181 logger.Debugf("creating partition on %q", devicePath) 182 if _, err := run("sgdisk", "-n", "1:0:-1", devicePath); err != nil { 183 return errors.Annotate(err, "sgdisk failed") 184 } 185 return nil 186 } 187 188 func createFilesystem(run runCommandFunc, devicePath string) error { 189 logger.Debugf("attempting to create filesystem on %q", devicePath) 190 mkfscmd := "mkfs." + defaultFilesystemType 191 _, err := run(mkfscmd, devicePath) 192 if err != nil { 193 return errors.Annotatef(err, "%s failed", mkfscmd) 194 } 195 logger.Infof("created filesystem on %q", devicePath) 196 return nil 197 } 198 199 func mountFilesystem(run runCommandFunc, dirFuncs dirFuncs, devicePath, mountPoint string, readOnly bool) error { 200 logger.Debugf("attempting to mount filesystem on %q at %q", devicePath, mountPoint) 201 if err := dirFuncs.mkDirAll(mountPoint, 0755); err != nil { 202 return errors.Annotate(err, "creating mount point") 203 } 204 mounted, mountSource, err := isMounted(dirFuncs, mountPoint) 205 if err != nil { 206 return errors.Trace(err) 207 } 208 if mounted { 209 logger.Debugf("filesystem on %q already mounted at %q", mountSource, mountPoint) 210 return nil 211 } 212 var args []string 213 if readOnly { 214 args = append(args, "-o", "ro") 215 } 216 args = append(args, devicePath, mountPoint) 217 if _, err := run("mount", args...); err != nil { 218 return errors.Annotate(err, "mount failed") 219 } 220 logger.Infof("mounted filesystem on %q at %q", devicePath, mountPoint) 221 return nil 222 } 223 224 func maybeUnmount(run runCommandFunc, dirFuncs dirFuncs, mountPoint string) error { 225 mounted, _, err := isMounted(dirFuncs, mountPoint) 226 if err != nil { 227 return errors.Trace(err) 228 } 229 if !mounted { 230 return nil 231 } 232 logger.Debugf("attempting to unmount filesystem at %q", mountPoint) 233 if _, err := run("umount", mountPoint); err != nil { 234 return errors.Annotate(err, "umount failed") 235 } 236 logger.Infof("unmounted filesystem at %q", mountPoint) 237 return nil 238 } 239 240 func isMounted(dirFuncs dirFuncs, mountPoint string) (bool, string, error) { 241 mountPointParent := filepath.Dir(mountPoint) 242 parentSource, err := dirFuncs.mountPointSource(mountPointParent) 243 if err != nil { 244 return false, "", errors.Trace(err) 245 } 246 source, err := dirFuncs.mountPointSource(mountPoint) 247 if err != nil { 248 return false, "", errors.Trace(err) 249 } 250 if source != parentSource { 251 // Already mounted. 252 return true, source, nil 253 } 254 return false, "", nil 255 } 256 257 // devicePath returns the device path for the given block device. 258 func devicePath(dev storage.BlockDevice) string { 259 return path.Join("/dev", dev.DeviceName) 260 } 261 262 // partitionDevicePath returns the device path for the first (and only) 263 // partition of the disk with the specified device path. 264 func partitionDevicePath(devicePath string) string { 265 return devicePath + "1" 266 } 267 268 // isDiskDevice reports whether or not the device is a full disk, as opposed 269 // to a partition or a loop device. We create a partition on disks to contain 270 // filesystems. 271 func isDiskDevice(devicePath string) bool { 272 var last rune 273 for _, r := range devicePath { 274 last = r 275 } 276 return !unicode.IsDigit(last) 277 }