mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 16:01:55 +00:00
Internally XFS uses allocation groups. Allocation groups have a maximum
size of 1 TiB - 1 block. For devices >= 4 TiB XFS uses max size
allocation groups. If a filesystem is extended and the last allocation
group is already at max size, a new allocation group is added. An
allocation group seems to require at least 64 4 KiB blocks.
For devices with integer TiB size (>4), this creates a filesystem that
has initially has 1 unused block per TiB size. The `resize` option
detects this unused space, and tries to resize the filesystem. The
xfs_growfs call is successful (exit 0), but does not increase the file
system size. This is detected as repeated change in the task.
Test case:
```
- hosts: localhost
tasks:
- ansible.builtin.command:
cmd: truncate -s 4T /media/xfs.img
creates: /media/xfs.img
notify: loopdev xfs
- ansible.builtin.meta: flush_handlers
- name: pickup xfs.img resize
ansible.builtin.command:
cmd: losetup -c /dev/loop0
changed_when: false
- community.general.filesystem:
dev: "/dev/loop0"
fstype: "xfs"
- ansible.posix.mount:
src: "/dev/loop0"
fstype: "xfs"
path: "/media/xfs"
state: "mounted"
# always shows a diff even for newly created filesystems
- community.general.filesystem:
dev: "/dev/loop0"
fstype: "xfs"
resizefs: true
handlers:
- name: loopdev xfs
ansible.builtin.command:
cmd: losetup /dev/loop0 /media/xfs.img
```
NB: If the last allocation group is not yet at max size, the filesystem
can be resized. Detecting this requires considering the XFS topology.
Other filesystems (at least ext4) also seem to require a minimum
increment after the initial device size, but seem to use the entire
device after initial creation.
Fun observation: creating a 64(+) TiB filesystem leaves a 64(+) block
gap at the end, that is allocated in a subsequent xfs_growfs call.
Co-authored-by: Johannes Naab <johannes.naab@hetzner-cloud.de>
738 lines
26 KiB
Python
738 lines
26 KiB
Python
#!/usr/bin/python
|
|
|
|
# Copyright (c) 2021, quidame <quidame@poivron.org>
|
|
# Copyright (c) 2013, Alexander Bulimov <lazywolf0@gmail.com>
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
DOCUMENTATION = r"""
|
|
author:
|
|
- Alexander Bulimov (@abulimov)
|
|
- quidame (@quidame)
|
|
module: filesystem
|
|
short_description: Makes a filesystem
|
|
description:
|
|
- This module creates a filesystem.
|
|
extends_documentation_fragment:
|
|
- community.general.attributes
|
|
attributes:
|
|
check_mode:
|
|
support: full
|
|
diff_mode:
|
|
support: none
|
|
options:
|
|
state:
|
|
description:
|
|
- If O(state=present), the filesystem is created if it does not already exist, that is the default behaviour if O(state)
|
|
is omitted.
|
|
- If O(state=absent), filesystem signatures on O(dev) are wiped if it contains a filesystem (as known by C(blkid)).
|
|
- When O(state=absent), all other options but O(dev) are ignored, and the module does not fail if the device O(dev)
|
|
does not actually exist.
|
|
type: str
|
|
choices: [present, absent]
|
|
default: present
|
|
version_added: 1.3.0
|
|
fstype:
|
|
choices: [bcachefs, btrfs, ext2, ext3, ext4, ext4dev, f2fs, lvm, ocfs2, reiserfs, xfs, vfat, swap, ufs]
|
|
description:
|
|
- Filesystem type to be created. This option is required with O(state=present) (or if O(state) is omitted).
|
|
- V(ufs) support has been added in community.general 3.4.0.
|
|
- V(bcachefs) support has been added in community.general 8.6.0.
|
|
type: str
|
|
aliases: [type]
|
|
dev:
|
|
description:
|
|
- Target path to block device (Linux) or character device (FreeBSD) or regular file (both).
|
|
- When setting Linux-specific filesystem types on FreeBSD, this module only works when applying to regular files, also known as
|
|
disk images.
|
|
- Currently V(lvm) (Linux-only) and V(ufs) (FreeBSD-only) do not support a regular file as their target O(dev).
|
|
- Support for character devices on FreeBSD has been added in community.general 3.4.0.
|
|
type: path
|
|
required: true
|
|
aliases: [device]
|
|
force:
|
|
description:
|
|
- If V(true), allows to create new filesystem on devices that already has filesystem.
|
|
type: bool
|
|
default: false
|
|
resizefs:
|
|
description:
|
|
- If V(true), if the block device and filesystem size differ, grow the filesystem into the space.
|
|
- >-
|
|
Supported when O(fstype) is one of: V(bcachefs), V(btrfs), V(ext2), V(ext3), V(ext4), V(ext4dev), V(f2fs), V(lvm), V(xfs), V(ufs) and V(vfat).
|
|
Attempts to resize other filesystem types fail.
|
|
- XFS only grows if mounted. Currently, the module is based on commands from C(util-linux) package to perform operations,
|
|
so resizing of XFS is not supported on FreeBSD systems.
|
|
- VFAT is likely to fail if C(fatresize < 1.04).
|
|
- Mutually exclusive with O(uuid).
|
|
type: bool
|
|
default: false
|
|
opts:
|
|
description:
|
|
- List of options to be passed to C(mkfs) command.
|
|
type: str
|
|
uuid:
|
|
description:
|
|
- Set filesystem's UUID to the given value.
|
|
- The UUID options specified in O(opts) take precedence over this value.
|
|
- See xfs_admin(8) (C(xfs)), tune2fs(8) (C(ext2), C(ext3), C(ext4), C(ext4dev)) for possible values.
|
|
- For O(fstype=lvm) the value is ignored, it resets the PV UUID if set.
|
|
- Supported for O(fstype) being one of V(bcachefs), V(ext2), V(ext3), V(ext4), V(ext4dev), V(lvm), or V(xfs).
|
|
- This is B(not idempotent). Specifying this option always results in a change.
|
|
- Mutually exclusive with O(resizefs).
|
|
type: str
|
|
version_added: 7.1.0
|
|
requirements:
|
|
- Uses specific tools related to the O(fstype) for creating or resizing a filesystem (from packages e2fsprogs, xfsprogs,
|
|
dosfstools, and so on).
|
|
- Uses generic tools mostly related to the Operating System (Linux or FreeBSD) or available on both, as C(blkid).
|
|
- On FreeBSD, either C(util-linux) or C(e2fsprogs) package is required.
|
|
notes:
|
|
- Potential filesystems on O(dev) are checked using C(blkid). In case C(blkid) is unable to detect a filesystem (and in
|
|
case C(fstyp) on FreeBSD is also unable to detect a filesystem), this filesystem is overwritten even if O(force=false).
|
|
- On FreeBSD systems, both C(e2fsprogs) and C(util-linux) packages provide a C(blkid) command that is compatible with this
|
|
module. However, these packages conflict with each other, and only the C(util-linux) package provides the command required
|
|
to not fail when O(state=absent).
|
|
seealso:
|
|
- module: community.general.filesize
|
|
- module: ansible.posix.mount
|
|
- name: xfs_admin(8) manpage for Linux
|
|
description: Manual page of the GNU/Linux's xfs_admin implementation.
|
|
link: https://man7.org/linux/man-pages/man8/xfs_admin.8.html
|
|
- name: tune2fs(8) manpage for Linux
|
|
description: Manual page of the GNU/Linux's tune2fs implementation.
|
|
link: https://man7.org/linux/man-pages/man8/tune2fs.8.html
|
|
"""
|
|
|
|
EXAMPLES = r"""
|
|
- name: Create a ext2 filesystem on /dev/sdb1
|
|
community.general.filesystem:
|
|
fstype: ext2
|
|
dev: /dev/sdb1
|
|
|
|
- name: Create a ext4 filesystem on /dev/sdb1 and check disk blocks
|
|
community.general.filesystem:
|
|
fstype: ext4
|
|
dev: /dev/sdb1
|
|
opts: -cc
|
|
|
|
- name: Blank filesystem signature on /dev/sdb1
|
|
community.general.filesystem:
|
|
dev: /dev/sdb1
|
|
state: absent
|
|
|
|
- name: Create a filesystem on top of a regular file
|
|
community.general.filesystem:
|
|
dev: /path/to/disk.img
|
|
fstype: vfat
|
|
|
|
- name: Reset an xfs filesystem UUID on /dev/sdb1
|
|
community.general.filesystem:
|
|
fstype: xfs
|
|
dev: /dev/sdb1
|
|
uuid: generate
|
|
|
|
- name: Reset an ext4 filesystem UUID on /dev/sdb1
|
|
community.general.filesystem:
|
|
fstype: ext4
|
|
dev: /dev/sdb1
|
|
uuid: random
|
|
|
|
- name: Reset an LVM filesystem (PV) UUID on /dev/sdc
|
|
community.general.filesystem:
|
|
fstype: lvm
|
|
dev: /dev/sdc
|
|
uuid: random
|
|
"""
|
|
|
|
import os
|
|
import platform
|
|
import re
|
|
import stat
|
|
|
|
from ansible.module_utils.basic import AnsibleModule
|
|
|
|
from ansible_collections.community.general.plugins.module_utils.version import LooseVersion
|
|
|
|
|
|
class Device:
|
|
def __init__(self, module, path):
|
|
self.module = module
|
|
self.path = path
|
|
|
|
def size(self):
|
|
"""Return size in bytes of device. Returns int"""
|
|
statinfo = os.stat(self.path)
|
|
if stat.S_ISBLK(statinfo.st_mode):
|
|
blockdev_cmd = self.module.get_bin_path("blockdev", required=True)
|
|
dummy, out, dummy = self.module.run_command([blockdev_cmd, "--getsize64", self.path], check_rc=True)
|
|
devsize_in_bytes = int(out)
|
|
elif stat.S_ISCHR(statinfo.st_mode) and platform.system() == "FreeBSD":
|
|
diskinfo_cmd = self.module.get_bin_path("diskinfo", required=True)
|
|
dummy, out, dummy = self.module.run_command([diskinfo_cmd, self.path], check_rc=True)
|
|
devsize_in_bytes = int(out.split()[2])
|
|
elif os.path.isfile(self.path):
|
|
devsize_in_bytes = os.path.getsize(self.path)
|
|
else:
|
|
self.module.fail_json(changed=False, msg=f"Target device not supported: {self}")
|
|
|
|
return devsize_in_bytes
|
|
|
|
def get_mountpoint(self):
|
|
"""Return (first) mountpoint of device. Returns None when not mounted."""
|
|
cmd_findmnt = self.module.get_bin_path("findmnt", required=True)
|
|
|
|
# find mountpoint
|
|
rc, mountpoint, dummy = self.module.run_command(
|
|
[cmd_findmnt, "--mtab", "--noheadings", "--output", "TARGET", "--source", self.path], check_rc=False
|
|
)
|
|
if rc != 0:
|
|
mountpoint = None
|
|
else:
|
|
mountpoint = mountpoint.split("\n")[0]
|
|
|
|
return mountpoint
|
|
|
|
def __str__(self):
|
|
return self.path
|
|
|
|
|
|
class Filesystem:
|
|
MKFS: str | None = None
|
|
MKFS_FORCE_FLAGS: list[str] | None = []
|
|
MKFS_SET_UUID_OPTIONS: list[str] | None = None
|
|
MKFS_SET_UUID_EXTRA_OPTIONS: list[str] | None = []
|
|
INFO: str | None = None
|
|
GROW: str | None = None
|
|
GROW_SLACK: int = 0
|
|
GROW_MAX_SPACE_FLAGS: list[str] | None = []
|
|
GROW_MOUNTPOINT_ONLY = False
|
|
CHANGE_UUID: str | None = None
|
|
CHANGE_UUID_OPTION: str | None = None
|
|
CHANGE_UUID_OPTION_HAS_ARG = True
|
|
|
|
LANG_ENV = {"LANG": "C", "LC_ALL": "C", "LC_MESSAGES": "C"}
|
|
|
|
def __init__(self, module):
|
|
self.module = module
|
|
|
|
@property
|
|
def fstype(self):
|
|
return type(self).__name__
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Return size in bytes of filesystem on device (integer).
|
|
Should query the info with a per-fstype command that can access the
|
|
device whenever it is mounted or not, and parse the command output.
|
|
Parser must ensure to return an integer, or raise a ValueError.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
def create(self, opts, dev, uuid=None):
|
|
if self.module.check_mode:
|
|
return
|
|
|
|
if uuid and self.MKFS_SET_UUID_OPTIONS:
|
|
if not (set(self.MKFS_SET_UUID_OPTIONS) & set(opts)):
|
|
opts += [self.MKFS_SET_UUID_OPTIONS[0], uuid] + self.MKFS_SET_UUID_EXTRA_OPTIONS
|
|
|
|
mkfs = self.module.get_bin_path(self.MKFS, required=True)
|
|
cmd = [mkfs] + self.MKFS_FORCE_FLAGS + opts + [str(dev)]
|
|
self.module.run_command(cmd, check_rc=True)
|
|
if uuid and self.CHANGE_UUID and self.MKFS_SET_UUID_OPTIONS is None:
|
|
self.change_uuid(new_uuid=uuid, dev=dev)
|
|
|
|
def wipefs(self, dev):
|
|
if self.module.check_mode:
|
|
return
|
|
|
|
# wipefs comes with util-linux package (as 'blockdev' & 'findmnt' above)
|
|
# that is ported to FreeBSD. The use of dd as a portable fallback is
|
|
# not doable here if it needs get_mountpoint() (to prevent corruption of
|
|
# a mounted filesystem), since 'findmnt' is not available on FreeBSD,
|
|
# even in util-linux port for this OS.
|
|
wipefs = self.module.get_bin_path("wipefs", required=True)
|
|
cmd = [wipefs, "--all", str(dev)]
|
|
self.module.run_command(cmd, check_rc=True)
|
|
|
|
def grow_cmd(self, target):
|
|
"""Build and return the resizefs commandline as list."""
|
|
cmdline = [self.module.get_bin_path(self.GROW, required=True)]
|
|
cmdline += self.GROW_MAX_SPACE_FLAGS + [target]
|
|
return cmdline
|
|
|
|
def grow(self, dev):
|
|
"""Get dev and fs size and compare. Returns stdout of used command."""
|
|
devsize_in_bytes = dev.size()
|
|
|
|
try:
|
|
fssize_in_bytes = self.get_fs_size(dev)
|
|
except NotImplementedError:
|
|
self.module.fail_json(msg=f"module does not support resizing {self.fstype} filesystem yet")
|
|
except ValueError as err:
|
|
self.module.warn(f"unable to process {self.INFO} output '{err}'")
|
|
self.module.fail_json(msg=f"unable to process {self.INFO} output for {dev}")
|
|
|
|
if fssize_in_bytes + self.GROW_SLACK >= devsize_in_bytes:
|
|
self.module.exit_json(changed=False, msg=f"{self.fstype} filesystem is using the whole device {dev}")
|
|
elif self.module.check_mode:
|
|
self.module.exit_json(changed=True, msg=f"resizing filesystem {self.fstype} on device {dev}")
|
|
|
|
if self.GROW_MOUNTPOINT_ONLY:
|
|
mountpoint = dev.get_mountpoint()
|
|
if not mountpoint:
|
|
self.module.fail_json(msg=f"{dev} needs to be mounted for {self.fstype} operations")
|
|
grow_target = mountpoint
|
|
else:
|
|
grow_target = str(dev)
|
|
|
|
dummy, out, dummy = self.module.run_command(self.grow_cmd(grow_target), check_rc=True)
|
|
return out
|
|
|
|
def change_uuid_cmd(self, new_uuid, target):
|
|
"""Build and return the UUID change command line as list."""
|
|
cmdline = [self.module.get_bin_path(self.CHANGE_UUID, required=True)]
|
|
if self.CHANGE_UUID_OPTION_HAS_ARG:
|
|
cmdline += [self.CHANGE_UUID_OPTION, new_uuid, target]
|
|
else:
|
|
cmdline += [self.CHANGE_UUID_OPTION, target]
|
|
return cmdline
|
|
|
|
def change_uuid(self, new_uuid, dev):
|
|
"""Change filesystem UUID. Returns stdout of used command"""
|
|
if self.module.check_mode:
|
|
self.module.exit_json(change=True, msg=f"Changing {self.fstype} filesystem UUID on device {dev}")
|
|
|
|
dummy, out, dummy = self.module.run_command(
|
|
self.change_uuid_cmd(new_uuid=new_uuid, target=str(dev)), check_rc=True
|
|
)
|
|
return out
|
|
|
|
|
|
class Ext(Filesystem):
|
|
MKFS_FORCE_FLAGS = ["-F"]
|
|
MKFS_SET_UUID_OPTIONS = ["-U"]
|
|
INFO = "tune2fs"
|
|
GROW = "resize2fs"
|
|
CHANGE_UUID = "tune2fs"
|
|
CHANGE_UUID_OPTION = "-U"
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get Block count and Block size and return their product."""
|
|
cmd = self.module.get_bin_path(self.INFO, required=True)
|
|
dummy, out, dummy = self.module.run_command([cmd, "-l", str(dev)], check_rc=True, environ_update=self.LANG_ENV)
|
|
|
|
block_count = block_size = None
|
|
for line in out.splitlines():
|
|
if "Block count:" in line:
|
|
block_count = int(line.split(":")[1].strip())
|
|
elif "Block size:" in line:
|
|
block_size = int(line.split(":")[1].strip())
|
|
if None not in (block_size, block_count):
|
|
break
|
|
else:
|
|
raise ValueError(repr(out))
|
|
|
|
return block_size * block_count
|
|
|
|
|
|
class Ext2(Ext):
|
|
MKFS = "mkfs.ext2"
|
|
|
|
|
|
class Ext3(Ext):
|
|
MKFS = "mkfs.ext3"
|
|
|
|
|
|
class Ext4(Ext):
|
|
MKFS = "mkfs.ext4"
|
|
|
|
|
|
class XFS(Filesystem):
|
|
MKFS = "mkfs.xfs"
|
|
MKFS_FORCE_FLAGS = ["-f"]
|
|
INFO = "xfs_info"
|
|
GROW = "xfs_growfs"
|
|
# XFS (defaults with 4KiB blocksize) requires at least 64 block of free
|
|
# space to add a new allocation group, avoid resizing (noop, but shown as
|
|
# diff) if the difference between the filesystem and the device is less
|
|
GROW_SLACK = 64 * 4096 - 1
|
|
GROW_MOUNTPOINT_ONLY = True
|
|
CHANGE_UUID = "xfs_admin"
|
|
CHANGE_UUID_OPTION = "-U"
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get bsize and blocks and return their product."""
|
|
cmdline = [self.module.get_bin_path(self.INFO, required=True)]
|
|
|
|
# Depending on the versions, xfs_info is able to get info from the
|
|
# device, whenever it is mounted or not, or only if unmounted, or
|
|
# only if mounted, or not at all. For any version until now, it is
|
|
# able to query info from the mountpoint. So try it first, and use
|
|
# device as the last resort: it may or may not work.
|
|
mountpoint = dev.get_mountpoint()
|
|
if mountpoint:
|
|
cmdline += [mountpoint]
|
|
else:
|
|
cmdline += [str(dev)]
|
|
dummy, out, dummy = self.module.run_command(cmdline, check_rc=True, environ_update=self.LANG_ENV)
|
|
|
|
block_size = block_count = None
|
|
for line in out.splitlines():
|
|
col = line.split("=")
|
|
if col[0].strip() == "data":
|
|
if col[1].strip() == "bsize":
|
|
block_size = int(col[2].split()[0])
|
|
if col[2].split()[1] == "blocks":
|
|
block_count = int(col[3].split(",")[0])
|
|
if None not in (block_size, block_count):
|
|
break
|
|
else:
|
|
raise ValueError(repr(out))
|
|
|
|
return block_size * block_count
|
|
|
|
|
|
class Reiserfs(Filesystem):
|
|
MKFS = "mkfs.reiserfs"
|
|
MKFS_FORCE_FLAGS = ["-q"]
|
|
|
|
|
|
class Bcachefs(Filesystem):
|
|
MKFS = "mkfs.bcachefs"
|
|
MKFS_FORCE_FLAGS = ["--force"]
|
|
MKFS_SET_UUID_OPTIONS = ["-U", "--uuid"]
|
|
INFO = "bcachefs"
|
|
GROW = "bcachefs"
|
|
GROW_MAX_SPACE_FLAGS = ["device", "resize"]
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Return size in bytes of filesystem on device (integer)."""
|
|
dummy, stdout, dummy = self.module.run_command(
|
|
[self.module.get_bin_path(self.INFO), "show-super", str(dev)], check_rc=True
|
|
)
|
|
|
|
for line in stdout.splitlines():
|
|
if "Size: " in line:
|
|
parts = line.split()
|
|
unit = parts[2]
|
|
|
|
base = None
|
|
exp = None
|
|
|
|
units_2 = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
|
|
units_10 = ["B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
|
|
try:
|
|
exp = units_2.index(unit)
|
|
base = 1024
|
|
except ValueError:
|
|
exp = units_10.index(unit)
|
|
base = 1000
|
|
|
|
if exp == 0:
|
|
value = int(parts[1])
|
|
else:
|
|
value = float(parts[1])
|
|
|
|
if base is not None and exp is not None:
|
|
return int(value * pow(base, exp))
|
|
|
|
raise ValueError(repr(stdout))
|
|
|
|
|
|
class Btrfs(Filesystem):
|
|
MKFS = "mkfs.btrfs"
|
|
INFO = "btrfs"
|
|
GROW = "btrfs"
|
|
GROW_MAX_SPACE_FLAGS = ["filesystem", "resize", "max"]
|
|
GROW_MOUNTPOINT_ONLY = True
|
|
|
|
def __init__(self, module):
|
|
super().__init__(module)
|
|
mkfs = self.module.get_bin_path(self.MKFS, required=True)
|
|
dummy, stdout, stderr = self.module.run_command([mkfs, "--version"], check_rc=True)
|
|
match = re.search(r" v([0-9.]+)", stdout)
|
|
if not match:
|
|
# v0.20-rc1 use stderr
|
|
match = re.search(r" v([0-9.]+)", stderr)
|
|
if match:
|
|
# v0.20-rc1 doesn't have --force parameter added in following version v3.12
|
|
if LooseVersion(match.group(1)) >= LooseVersion("3.12"):
|
|
self.MKFS_FORCE_FLAGS = ["-f"]
|
|
else:
|
|
# assume version is greater or equal to 3.12
|
|
self.MKFS_FORCE_FLAGS = ["-f"]
|
|
self.module.warn(f"Unable to identify mkfs.btrfs version ({stdout!r}, {stderr!r})")
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Return size in bytes of filesystem on device (integer)."""
|
|
mountpoint = dev.get_mountpoint()
|
|
if not mountpoint:
|
|
self.module.fail_json(msg=f"{dev} needs to be mounted for {self.fstype} operations")
|
|
|
|
dummy, stdout, dummy = self.module.run_command(
|
|
[self.module.get_bin_path(self.INFO), "filesystem", "usage", "-b", mountpoint], check_rc=True
|
|
)
|
|
for line in stdout.splitlines():
|
|
if "Device size" in line:
|
|
return int(line.split()[-1])
|
|
raise ValueError(repr(stdout))
|
|
|
|
|
|
class Ocfs2(Filesystem):
|
|
MKFS = "mkfs.ocfs2"
|
|
MKFS_FORCE_FLAGS = ["-Fx"]
|
|
|
|
|
|
class F2fs(Filesystem):
|
|
MKFS = "mkfs.f2fs"
|
|
INFO = "dump.f2fs"
|
|
GROW = "resize.f2fs"
|
|
|
|
def __init__(self, module):
|
|
super().__init__(module)
|
|
mkfs = self.module.get_bin_path(self.MKFS, required=True)
|
|
dummy, out, dummy = self.module.run_command([mkfs, os.devnull], check_rc=False, environ_update=self.LANG_ENV)
|
|
# Looking for " F2FS-tools: mkfs.f2fs Ver: 1.10.0 (2018-01-30)"
|
|
# mkfs.f2fs displays version since v1.2.0
|
|
match = re.search(r"F2FS-tools: mkfs.f2fs Ver: ([0-9.]+) \(", out)
|
|
if match is not None:
|
|
# Since 1.9.0, mkfs.f2fs check overwrite before make filesystem
|
|
# before that version -f switch wasn't used
|
|
if LooseVersion(match.group(1)) >= LooseVersion("1.9.0"):
|
|
self.MKFS_FORCE_FLAGS = ["-f"]
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get sector size and total FS sectors and return their product."""
|
|
cmd = self.module.get_bin_path(self.INFO, required=True)
|
|
dummy, out, dummy = self.module.run_command([cmd, str(dev)], check_rc=True, environ_update=self.LANG_ENV)
|
|
sector_size = sector_count = None
|
|
for line in out.splitlines():
|
|
if "Info: sector size = " in line:
|
|
# expected: 'Info: sector size = 512'
|
|
sector_size = int(line.split()[4])
|
|
elif "Info: total FS sectors = " in line:
|
|
# expected: 'Info: total FS sectors = 102400 (50 MB)'
|
|
sector_count = int(line.split()[5])
|
|
if None not in (sector_size, sector_count):
|
|
break
|
|
else:
|
|
raise ValueError(repr(out))
|
|
|
|
return sector_size * sector_count
|
|
|
|
|
|
class VFAT(Filesystem):
|
|
INFO = "fatresize"
|
|
GROW = "fatresize"
|
|
GROW_MAX_SPACE_FLAGS = ["-s", "max"]
|
|
|
|
def __init__(self, module):
|
|
super().__init__(module)
|
|
if platform.system() == "FreeBSD":
|
|
self.MKFS = "newfs_msdos"
|
|
else:
|
|
self.MKFS = "mkfs.vfat"
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get and return size of filesystem, in bytes."""
|
|
cmd = self.module.get_bin_path(self.INFO, required=True)
|
|
dummy, out, dummy = self.module.run_command(
|
|
[cmd, "--info", str(dev)], check_rc=True, environ_update=self.LANG_ENV
|
|
)
|
|
fssize = None
|
|
for line in out.splitlines()[1:]:
|
|
parts = line.split(":", 1)
|
|
if len(parts) < 2:
|
|
continue
|
|
param, value = parts
|
|
if param.strip() in ("Size", "Cur size"):
|
|
fssize = int(value.strip())
|
|
break
|
|
else:
|
|
raise ValueError(repr(out))
|
|
|
|
return fssize
|
|
|
|
|
|
class LVM(Filesystem):
|
|
MKFS = "pvcreate"
|
|
MKFS_FORCE_FLAGS = ["-f"]
|
|
MKFS_SET_UUID_OPTIONS = ["-u", "--uuid"]
|
|
MKFS_SET_UUID_EXTRA_OPTIONS = ["--norestorefile"]
|
|
INFO = "pvs"
|
|
GROW = "pvresize"
|
|
CHANGE_UUID = "pvchange"
|
|
CHANGE_UUID_OPTION = "-u"
|
|
CHANGE_UUID_OPTION_HAS_ARG = False
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get and return PV size, in bytes."""
|
|
cmd = self.module.get_bin_path(self.INFO, required=True)
|
|
dummy, size, dummy = self.module.run_command(
|
|
[cmd, "--noheadings", "-o", "pv_size", "--units", "b", "--nosuffix", str(dev)], check_rc=True
|
|
)
|
|
pv_size = int(size)
|
|
return pv_size
|
|
|
|
|
|
class Swap(Filesystem):
|
|
MKFS = "mkswap"
|
|
MKFS_FORCE_FLAGS = ["-f"]
|
|
|
|
|
|
class UFS(Filesystem):
|
|
MKFS = "newfs"
|
|
INFO = "dumpfs"
|
|
GROW = "growfs"
|
|
GROW_MAX_SPACE_FLAGS = ["-y"]
|
|
|
|
def get_fs_size(self, dev):
|
|
"""Get providersize and fragment size and return their product."""
|
|
cmd = self.module.get_bin_path(self.INFO, required=True)
|
|
dummy, out, dummy = self.module.run_command([cmd, str(dev)], check_rc=True, environ_update=self.LANG_ENV)
|
|
|
|
fragmentsize = providersize = None
|
|
for line in out.splitlines():
|
|
if line.startswith("fsize"):
|
|
fragmentsize = int(line.split()[1])
|
|
elif "providersize" in line:
|
|
providersize = int(line.split()[-1])
|
|
if None not in (fragmentsize, providersize):
|
|
break
|
|
else:
|
|
raise ValueError(repr(out))
|
|
|
|
return fragmentsize * providersize
|
|
|
|
|
|
FILESYSTEMS = {
|
|
"bcachefs": Bcachefs,
|
|
"ext2": Ext2,
|
|
"ext3": Ext3,
|
|
"ext4": Ext4,
|
|
"ext4dev": Ext4,
|
|
"f2fs": F2fs,
|
|
"reiserfs": Reiserfs,
|
|
"xfs": XFS,
|
|
"btrfs": Btrfs,
|
|
"vfat": VFAT,
|
|
"ocfs2": Ocfs2,
|
|
"LVM2_member": LVM,
|
|
"swap": Swap,
|
|
"ufs": UFS,
|
|
}
|
|
|
|
|
|
def main():
|
|
friendly_names = {
|
|
"lvm": "LVM2_member",
|
|
}
|
|
|
|
fstypes = set(FILESYSTEMS.keys()) - set(friendly_names.values()) | set(friendly_names.keys())
|
|
|
|
# There is no "single command" to manipulate filesystems, so we map them all out and their options
|
|
module = AnsibleModule(
|
|
argument_spec=dict(
|
|
state=dict(type="str", default="present", choices=["present", "absent"]),
|
|
fstype=dict(type="str", aliases=["type"], choices=list(fstypes)),
|
|
dev=dict(type="path", required=True, aliases=["device"]),
|
|
opts=dict(type="str"),
|
|
force=dict(type="bool", default=False),
|
|
resizefs=dict(type="bool", default=False),
|
|
uuid=dict(type="str"),
|
|
),
|
|
required_if=[("state", "present", ["fstype"])],
|
|
mutually_exclusive=[
|
|
("resizefs", "uuid"),
|
|
],
|
|
supports_check_mode=True,
|
|
)
|
|
|
|
state = module.params["state"]
|
|
dev = module.params["dev"]
|
|
fstype = module.params["fstype"]
|
|
opts = module.params["opts"]
|
|
force = module.params["force"]
|
|
resizefs = module.params["resizefs"]
|
|
uuid = module.params["uuid"]
|
|
|
|
mkfs_opts = []
|
|
if opts is not None:
|
|
mkfs_opts = opts.split()
|
|
|
|
changed = False
|
|
|
|
if not os.path.exists(dev):
|
|
msg = f"Device {dev} not found."
|
|
if state == "present":
|
|
module.fail_json(msg=msg)
|
|
else:
|
|
module.exit_json(msg=msg)
|
|
|
|
dev = Device(module, dev)
|
|
|
|
# In case blkid/fstyp isn't able to identify an existing filesystem, device
|
|
# is considered as empty, then this existing filesystem would be overwritten
|
|
# even if force isn't enabled.
|
|
cmd = module.get_bin_path("blkid", required=True)
|
|
rc, raw_fs, err = module.run_command([cmd, "-c", os.devnull, "-o", "value", "-s", "TYPE", str(dev)])
|
|
fs = raw_fs.strip()
|
|
if not fs and platform.system() == "FreeBSD":
|
|
cmd = module.get_bin_path("fstyp", required=True)
|
|
rc, raw_fs, err = module.run_command([cmd, str(dev)])
|
|
fs = raw_fs.strip()
|
|
|
|
if state == "present":
|
|
if fstype in friendly_names:
|
|
fstype = friendly_names[fstype]
|
|
|
|
try:
|
|
klass = FILESYSTEMS[fstype]
|
|
except KeyError:
|
|
module.fail_json(changed=False, msg=f"module does not support this filesystem ({fstype}) yet.")
|
|
|
|
filesystem = klass(module)
|
|
|
|
if uuid and not (filesystem.CHANGE_UUID or filesystem.MKFS_SET_UUID_OPTIONS):
|
|
module.fail_json(
|
|
changed=False, msg=f"module does not support UUID option for this filesystem ({fstype}) yet."
|
|
)
|
|
|
|
same_fs = fs and FILESYSTEMS.get(fs) == FILESYSTEMS[fstype]
|
|
if same_fs and not resizefs and not uuid and not force:
|
|
module.exit_json(changed=False)
|
|
elif same_fs:
|
|
if resizefs:
|
|
if not filesystem.GROW:
|
|
module.fail_json(changed=False, msg=f"module does not support resizing {fstype} filesystem yet.")
|
|
|
|
out = filesystem.grow(dev)
|
|
|
|
module.exit_json(changed=True, msg=out)
|
|
elif uuid:
|
|
out = filesystem.change_uuid(new_uuid=uuid, dev=dev)
|
|
|
|
module.exit_json(changed=True, msg=out)
|
|
elif fs and not force:
|
|
module.fail_json(msg=f"'{dev}' is already used as {fs}, use force=true to overwrite", rc=rc, err=err)
|
|
|
|
# create fs
|
|
filesystem.create(opts=mkfs_opts, dev=dev, uuid=uuid)
|
|
changed = True
|
|
|
|
elif fs:
|
|
# wipe fs signatures
|
|
filesystem = Filesystem(module)
|
|
filesystem.wipefs(dev)
|
|
changed = True
|
|
|
|
module.exit_json(changed=changed)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|