Mini Shell
"""
Manage BTRFS file systems.
:maintainer: Alberto Planas <aplanas@suse.com>
:maturity: new
:depends: None
:platform: Linux
"""
import functools
import logging
import os.path
import tempfile
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
__virtualname__ = "btrfs"
def _mount(device, use_default):
"""
Mount the device in a temporary place.
"""
opts = "defaults" if use_default else "subvol=/"
dest = tempfile.mkdtemp()
res = __states__["mount.mounted"](
dest, device=device, fstype="btrfs", opts=opts, persist=False
)
if not res["result"]:
log.error("Cannot mount device %s in %s", device, dest)
_umount(dest)
return None
return dest
def _umount(path):
"""
Umount and clean the temporary place.
"""
__states__["mount.unmounted"](path)
__utils__["files.rm_rf"](path)
def _is_default(path, dest, name):
"""
Check if the subvolume is the current default.
"""
subvol_id = __salt__["btrfs.subvolume_show"](path)[name]["subvolume id"]
def_id = __salt__["btrfs.subvolume_get_default"](dest)["id"]
return subvol_id == def_id
def _set_default(path, dest, name):
"""
Set the subvolume as the current default.
"""
subvol_id = __salt__["btrfs.subvolume_show"](path)[name]["subvolume id"]
return __salt__["btrfs.subvolume_set_default"](subvol_id, dest)
def _is_cow(path):
"""
Check if the subvolume is copy on write
"""
dirname = os.path.dirname(path)
return "C" not in __salt__["file.lsattr"](dirname)[path]
def _unset_cow(path):
"""
Disable the copy on write in a subvolume
"""
return __salt__["file.chattr"](path, operator="add", attributes="C")
def __mount_device(action):
"""
Small decorator to makes sure that the mount and umount happends in
a transactional way.
"""
@functools.wraps(action)
def wrapper(*args, **kwargs):
name = kwargs.get("name", args[0] if args else None)
device = kwargs.get("device", args[1] if len(args) > 1 else None)
use_default = kwargs.get("use_default", False)
ret = {
"name": name,
"result": False,
"changes": {},
"comment": ["Some error happends during the operation."],
}
try:
if device:
dest = _mount(device, use_default)
if not dest:
msg = f"Device {device} cannot be mounted"
ret["comment"].append(msg)
kwargs["__dest"] = dest
ret = action(*args, **kwargs)
except Exception as exc: # pylint: disable=broad-except
log.error("Exception raised while mounting device: %s", exc, exc_info=True)
ret["comment"].append(exc)
finally:
if device:
_umount(dest)
return ret
return wrapper
@__mount_device
def subvolume_created(
name,
device,
qgroupids=None,
set_default=False,
copy_on_write=True,
force_set_default=True,
__dest=None,
):
"""
Makes sure that a btrfs subvolume is present.
name
Name of the subvolume to add
device
Device where to create the subvolume
qgroupids
Add the newly created subcolume to a qgroup. This parameter
is a list
set_default
If True, this new subvolume will be set as default when
mounted, unless subvol option in mount is used
copy_on_write
If false, set the subvolume with chattr +C
force_set_default
If false and the subvolume is already present, it will not
force it as default if ``set_default`` is True
"""
ret = {
"name": name,
"result": False,
"changes": {},
"comment": [],
}
path = os.path.join(__dest, name)
exists = __salt__["btrfs.subvolume_exists"](path)
if exists:
ret["comment"].append(f"Subvolume {name} already present")
# Resolve first the test case. The check is not complete, but at
# least we will report if a subvolume needs to be created. Can
# happend that the subvolume is there, but we also need to set it
# as default, or persist in fstab.
if __opts__["test"]:
ret["result"] = None
if not exists:
ret["changes"][name] = f"Subvolume {name} will be created"
return ret
if not exists:
# Create the directories where the subvolume lives
_path = os.path.dirname(path)
res = __states__["file.directory"](_path, makedirs=True)
if not res["result"]:
ret["comment"].append(f"Error creating {_path} directory")
return ret
try:
__salt__["btrfs.subvolume_create"](name, dest=__dest, qgroupids=qgroupids)
except CommandExecutionError:
ret["comment"].append(f"Error creating subvolume {name}")
return ret
ret["changes"][name] = f"Created subvolume {name}"
# If the volume was already present, we can opt-out the check for
# default subvolume.
if (
(not exists or (exists and force_set_default))
and set_default
and not _is_default(path, __dest, name)
):
ret["changes"][name + "_default"] = _set_default(path, __dest, name)
if not copy_on_write and _is_cow(path):
ret["changes"][name + "_no_cow"] = _unset_cow(path)
ret["result"] = True
return ret
@__mount_device
def subvolume_deleted(name, device, commit=False, __dest=None):
"""
Makes sure that a btrfs subvolume is removed.
name
Name of the subvolume to remove
device
Device where to remove the subvolume
commit
Wait until the transaction is over
"""
ret = {
"name": name,
"result": False,
"changes": {},
"comment": [],
}
path = os.path.join(__dest, name)
exists = __salt__["btrfs.subvolume_exists"](path)
if not exists:
ret["comment"].append(f"Subvolume {name} already missing")
if __opts__["test"]:
ret["result"] = None
if exists:
ret["changes"][name] = f"Subvolume {name} will be removed"
return ret
# If commit is set, we wait until all is over
commit = "after" if commit else None
if not exists:
try:
__salt__["btrfs.subvolume_delete"](path, commit=commit)
except CommandExecutionError:
ret["comment"].append(f"Error removing subvolume {name}")
return ret
ret["changes"][name] = f"Removed subvolume {name}"
ret["result"] = True
return ret
def _diff_properties(expected, current):
"""Calculate the difference between the current and the expected
properties
* 'expected' is expressed in a dictionary like: {'property': value}
* 'current' contains the same format retuned by 'btrfs.properties'
If the property is not available, will throw an exception.
"""
difference = {}
for _property, value in expected.items():
current_value = current[_property]["value"]
if value is False and current_value == "N/A":
needs_update = False
elif value != current_value:
needs_update = True
else:
needs_update = False
if needs_update:
difference[_property] = value
return difference
@__mount_device
def properties(name, device, use_default=False, __dest=None, **properties):
"""
Makes sure that a list of properties are set in a subvolume, file
or device.
name
Name of the object to change
device
Device where the object lives, if None, the device will be in
name
use_default
If True, this subvolume will be resolved to the default
subvolume assigned during the create operation
properties
Dictionary of properties
Valid properties are 'ro', 'label' or 'compression'. Check the
documentation to see where those properties are valid for each
object.
"""
ret = {
"name": name,
"result": False,
"changes": {},
"comment": [],
}
# 'name' will have always the name of the object that we want to
# change, but if the object is a device, we do not repeat it again
# in 'device'. This makes device sometimes optional.
if device:
if os.path.isabs(name):
path = os.path.join(__dest, os.path.relpath(name, os.path.sep))
else:
path = os.path.join(__dest, name)
else:
path = name
if not os.path.exists(path):
ret["comment"].append(f"Object {name} not found")
return ret
# Convert the booleans to lowercase
properties = {
k: v if type(v) is not bool else str(v).lower() for k, v in properties.items()
}
current_properties = {}
try:
current_properties = __salt__["btrfs.properties"](path)
except CommandExecutionError as e:
ret["comment"].append(f"Error reading properties from {name}")
ret["comment"].append(f"Current error {e}")
return ret
try:
properties_to_set = _diff_properties(properties, current_properties)
except KeyError:
ret["comment"].append(f"Some property not found in {name}")
return ret
if __opts__["test"]:
ret["result"] = None
if properties_to_set:
ret["changes"] = properties_to_set
else:
msg = f"No properties will be changed in {name}"
ret["comment"].append(msg)
return ret
if properties_to_set:
_properties = ",".join(f"{k}={v}" for k, v in properties_to_set.items())
__salt__["btrfs.properties"](path, set=_properties)
current_properties = __salt__["btrfs.properties"](path)
properties_failed = _diff_properties(properties, current_properties)
if properties_failed:
msg = "Properties {} failed to be changed in {}".format(
properties_failed, name
)
ret["comment"].append(msg)
return ret
ret["comment"].append(f"Properties changed in {name}")
ret["changes"] = properties_to_set
else:
ret["comment"].append(f"Properties not changed in {name}")
ret["result"] = True
return ret
Zerion Mini Shell 1.0