[blivet][PATCH 2/5] Add tmpfs support (#918621)

Martin Kolman mkolman at redhat.com
Thu Nov 7 17:52:12 UTC 2013


Add support for creating tmpfs mounts, mounting/unmounting
them, checking free space on them and adding them to fstab.

Tmpfs is a filesystem that lives in the Kernel page cache
and stores data primarily in RAM and in swap once RAM runs out.

Data stored on tmpfs mounts will not survive a system
reboot/crash/shutdown.

About tmpfs mount size:
- if no size is specified, the size will be 50% by default
- if size is specified, the mount will have this size
-> there is no limit on the size on tmpfs mounts
-> just note that once it fills RAM (and any swap),
the system will grind to a halt
- grow and maxsize are not supported
- you can use use filesystem options to set size in percent:

fsopts == "size=25%"

This will make the tmpfs mount take up
to 25% percent of system RAM.

What operations are supported:
-> adding tmpfs mounts to fstab
-> mounting and unmounting tmpfs mounts
-> checking free space on mounted tmpfs mounts
-> resizing mounted tmpfs mounts

Resolves: rhbz#472127
Signed-off-by: Martin Kolman <mkolman at redhat.com>
---
 blivet/__init__.py     |   4 ++
 blivet/deviceaction.py |  10 ++-
 blivet/devices.py      |  27 ++++++++
 blivet/formats/fs.py   | 170 +++++++++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 202 insertions(+), 9 deletions(-)

diff --git a/blivet/__init__.py b/blivet/__init__.py
index 327274c..d7c1572 100644
--- a/blivet/__init__.py
+++ b/blivet/__init__.py
@@ -1186,6 +1186,10 @@ class Blivet(object):
         kwargs["subvol"] = True
         return self.newBTRFS(*args, **kwargs)
 
+    def newTmpFS(self, *args, **kwargs):
+        """ Return a new TmpFSDevice. """
+        return TmpFSDevice(*args, **kwargs)
+
     def createDevice(self, device):
         """ Schedule creation of a device.
 
diff --git a/blivet/deviceaction.py b/blivet/deviceaction.py
index 202a66c..a53fdb5 100644
--- a/blivet/deviceaction.py
+++ b/blivet/deviceaction.py
@@ -473,8 +473,14 @@ class ActionCreateFormat(DeviceAction):
             udev_settle()
             self.device.updateSysfsPath()
             info = udev_get_block_device(self.device.sysfsPath)
-            self.device.format.uuid = udev_device_get_uuid(info)
-            self.device.deviceLinks = udev_device_get_symlinks(info)
+            # only do this if the format has a device known to udev
+            # (the format might not have a normal device at all)
+            if info:
+                self.device.format.uuid = udev_device_get_uuid(info)
+                self.device.deviceLinks = udev_device_get_symlinks(info)
+            elif self.device.format.type != "tmpfs":
+                # udev lookup failing is a serious issue for anything other than tmpfs
+                log.error("udev lookup failed for device: %s" % self.device)
 
     def cancel(self):
         self.device.format = self.origFormat
diff --git a/blivet/devices.py b/blivet/devices.py
index d620eaf..5794d95 100644
--- a/blivet/devices.py
+++ b/blivet/devices.py
@@ -3768,6 +3768,33 @@ class NoDevice(StorageDevice):
         self._preDestroy()
 
 
+class TmpFSDevice(NoDevice):
+    """ A nodev device for a tmpfs filesystem. """
+    _type = "tmpfs"
+
+    def __init__(self, *args, **kwargs):
+        """Create a tmpfs device"""
+        format = kwargs.get('format')
+        NoDevice.__init__(self, format)
+        # the tmpfs device does not exist until mounted
+        self.exists = False
+        self._size = kwargs["size"]
+        self._targetSize = self._size
+
+    @property
+    def size(self):
+        if self._size is not None:
+            return self._size
+        elif self.format:
+            return self.format.size
+        else:
+            return 0
+
+    @property
+    def fstabSpec(self):
+        return self._type
+
+
 class FileDevice(StorageDevice):
     """ A file on a filesystem.
 
diff --git a/blivet/formats/fs.py b/blivet/formats/fs.py
index 04270f0..20ea1ac 100644
--- a/blivet/formats/fs.py
+++ b/blivet/formats/fs.py
@@ -21,11 +21,7 @@
 #                    David Cantrell <dcantrell at redhat.com>
 #
 
-""" Filesystem classes for use by anaconda.
-
-    TODO:
-        - bug 472127: allow creation of tmpfs filesystems (/tmp, /var/tmp, &c)
-"""
+""" Filesystem classes. """
 import math
 import os
 import sys
@@ -39,6 +35,7 @@ from .. import platform
 from ..flags import flags
 from parted import fileSystemType
 from ..storage_log import log_method_call
+from .. import arch
 
 import logging
 log = logging.getLogger("blivet")
@@ -54,6 +51,8 @@ except OSError:
 # these are for converting to/from SI for ntfsresize
 mb = 1000 * 1000.0
 mib = 1024 * 1024.0
+gib = 1024 * 1024 * 1024.0
+
 
 fs_configs = {}
 
@@ -429,7 +428,8 @@ class FS(DeviceFormat):
         if not self.resizefsProg:
             return
 
-        if not os.path.exists(self.device):
+        # tmpfs mounts don't need an existing device node
+        if not self.device == "tmpfs" and not os.path.exists(self.device):
             raise FSResizeError("device does not exist", self.device)
 
         self.doCheck()
@@ -1405,7 +1405,7 @@ class NoDevFS(FS):
     def __init__(self, *args, **kwargs):
         FS.__init__(self, *args, **kwargs)
         self.exists = True
-        self.device = self.type
+        self.device = self._type
 
     def _setDevice(self, devspec):
         self._device = devspec
@@ -1450,6 +1450,162 @@ register_device_format(SysFS)
 
 class TmpFS(NoDevFS):
     _type = "tmpfs"
+    _supported = True
+    _resizable = True
+    # remounting can be used to change
+    # the size of a live tmpfs mount
+    _resizefs = "mount"
+    # as tmpfs is part of the Linux kernel,
+    # it is Linux-native
+    _linuxNative = True
+    # empty tmpfs has zero overhead
+    _minInstanceSize = 0
+    # tmpfs really does not occupy any space by itself
+    _minSize = 0
+    # in a sense, I guess tmpfs is formattable
+    # in the regard that the format is automatically created
+    # once mounted
+    _formattable = True
+
+    def __init__(self, *args, **kwargs):
+        NoDevFS.__init__(self, *args, **kwargs)
+        self.exists = True
+        self._device = "tmpfs"
+
+        # according to the following Kernel ML thread:
+        # http://www.gossamer-threads.com/lists/linux/kernel/875278
+        # maximum tmpfs mount size is 16TB on 32 bit systems
+        # and 16EB on 64 bit systems
+        bits = arch.bits()
+        if bits == 32:
+            self._maxSize = 16 * 1024 * 1024
+        elif bits == 64:
+            self._maxSize = 16 * 1024 * 1024 * 1024 * 1024
+        # if the architecture is other than 32 or 64 bit or unknown
+        # just use the default maxsize, which is 0, this disables
+        # resizing but other operations such as mounting should work fine
+
+        # check if fixed filesystem size has been specified,
+        # if no size is specified, tmpfs will by default
+        # be limited to half of the system RAM
+        # (sizes of all tmpfs mounts are independent)
+        fsoptions = kwargs.get("mountopts")
+        system_ram = util.total_memory() / 1024  # kB to MB
+        self._size_option = ""
+        if fsoptions:
+            # some mount options were specified, replace the default value
+            self._options = fsoptions
+        if self._size:
+            # absolute size for the tmpfs mount has been specified
+            self._size_option = "size=%dm" % self._size
+        else:
+            # if no size option is specified, the tmpfs mount size will be 50%
+            # of system RAM by default
+            self._size = system_ram*0.5
+
+    def create(self, *args, **kwargs):
+        """ A filesystem is created automatically once tmpfs is mounted. """
+        pass
+
+    def destroy(self, *args, **kwargs):
+        """ The device and its filesystem are automatically destroyed once the
+        mountpoint is unmounted.
+        """
+        pass
+
+    @property
+    def mountable(self):
+        return True
+
+    def _getOptions(self):
+        # if the size option string is defined,
+        # append it to options
+        # (we need to separate the size option string
+        # from the other options, as the size might change
+        # when the filesystem is resized;
+        # replacing the size option in the otherwise free-form
+        # options string would get messy fast)
+        opts = ",".join([o for o in [self._options, self._size_option] if o])
+        return opts or "defaults"
+
+    def _setOptions(self, options):
+        self._options = options
+
+    # override the options property
+    # so that the size and other options
+    # are correctly added to fstab
+    options = property(_getOptions, _setOptions)
+
+    @property
+    def size(self):
+        return self._size
+
+    @property
+    def minSize(self):
+        """ The minimum filesystem size in megabytes. """
+        return self._minInstanceSize
+
+    @property
+    def free(self):
+        free_space = 0
+        if self._mountpoint:
+            # If self._mountpoint is defined, it means this tmpfs mount
+            # has been mounted and there is a path we can use as a handle to
+            # look-up the free space on the filesystem.
+            # When running with changeroot, such as during installation,
+            # self._mountpoint is set to the full changeroot path once mounted,
+            # so even with changeroot, statvfs should still work fine.
+            st = os.statvfs(self._mountpoint)
+            free_space = st.f_bavail * st.f_frsize/1024/1024  # blocks to MB
+        else:
+            # Free might be called even if the tmpfs mount has not been
+            # mounted yet, in this case just return the size set for the mount.
+            # Once mounted, the tmpfs mount will be empty
+            # and therefore free space will correspond to its size.
+            free_space = self._size
+        return free_space
+
+    def _getDevice(self):
+        """ All the tmpfs mounts use the same "tmpfs" device. """
+        return self._type
+
+    def _setDevice(self, value):
+        # the DeviceFormat parent class does a
+        # self.device = kwargs["device"]
+        # assignment, so we need a setter for the
+        # device property, but as the device is always the
+        # same, nothing actually needs to be set
+        pass
+
+    device = property(_getDevice, _setDevice)
+
+    @property
+    def resizeArgs(self):
+        # a live tmpfs mount can be resized by remounting it with
+        # the mount command
+
+        # add the remount flag, size and any options
+        remount_options = 'remount,size=%dm' % self.targetSize
+        # if any mount options are defined, append them
+        if self._options:
+            remount_options = "%s,%s" % (remount_options, self._options)
+        return ['-o', remount_options, self._type, self._mountpoint]
+
+    def doResize(self):
+        # we need to override doResize, because the
+        # self._size_option string needs to be updated in case
+        # the tmpfs mount is successfully resized, using the new
+        # self._size value
+        original_size = self._size
+        FS.doResize(self)
+        # after a successful resize, self._size
+        # is set to be equal to self.targetSize,
+        # so it can be used to check if resize took place
+        if original_size != self._size:
+            # update the size option string
+            # -> please note that resizing always sets the
+            # size of this tmpfs mount to an absolute value
+            self._size_option = "size=%dm" % self._size
 
 register_device_format(TmpFS)
 
-- 
1.8.3.1



More information about the anaconda-patches mailing list