Mini Shell
#
# The MIT License (MIT)
# Copyright (C) 2014 SUSE LLC
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
"""
Module for managing XFS file systems.
"""
import logging
import os
import re
import time
import salt.utils.data
import salt.utils.files
import salt.utils.path
import salt.utils.platform
from salt.exceptions import CommandExecutionError
log = logging.getLogger(__name__)
def __virtual__():
"""
Only work on POSIX-like systems
"""
return not salt.utils.platform.is_windows() and __grains__.get("kernel") == "Linux"
def _verify_run(out, cmd=None):
"""
Crash to the log if command execution was not successful.
"""
if out.get("retcode", 0) and out["stderr"]:
if cmd:
log.debug('Command: "%s"', cmd)
log.debug("Return code: %s", out.get("retcode"))
log.debug("Error output:\n%s", out.get("stderr", "N/A"))
raise CommandExecutionError(out["stderr"])
def _xfs_info_get_kv(serialized):
"""
Parse one line of the XFS info output.
"""
# No need to know sub-elements here
if serialized.startswith("="):
serialized = serialized[1:].strip()
serialized = serialized.replace(" = ", "=*** ").replace(" =", "=")
# Keywords has no spaces, values do
opt = []
for tkn in serialized.split(" "):
if not opt or "=" in tkn:
opt.append(tkn)
else:
opt[len(opt) - 1] = opt[len(opt) - 1] + " " + tkn
# Preserve ordering
return [tuple(items.split("=")) for items in opt]
def _parse_xfs_info(data):
"""
Parse output from "xfs_info" or "xfs_growfs -n".
"""
ret = {}
spr = re.compile(r"\s+")
entry = None
for line in [spr.sub(" ", l).strip().replace(", ", " ") for l in data.split("\n")]:
if not line or "=" not in line:
continue
nfo = _xfs_info_get_kv(line)
if not line.startswith("="):
entry = nfo.pop(0)
ret[entry[0]] = {"section": entry[(entry[1] != "***" and 1 or 0)]}
ret[entry[0]].update(dict(nfo))
return ret
def info(device):
"""
Get filesystem geometry information.
CLI Example:
.. code-block:: bash
salt '*' xfs.info /dev/sda1
"""
out = __salt__["cmd.run_all"](f"xfs_info {device}")
if out.get("stderr"):
raise CommandExecutionError(out["stderr"].replace("xfs_info:", "").strip())
return _parse_xfs_info(out["stdout"])
def _xfsdump_output(data):
"""
Parse CLI output of the xfsdump utility.
"""
out = {}
summary = []
summary_block = False
for line in [l.strip() for l in data.split("\n") if l.strip()]:
line = re.sub("^xfsdump: ", "", line)
if line.startswith("session id:"):
out["Session ID"] = line.split(" ")[-1]
elif line.startswith("session label:"):
out["Session label"] = re.sub("^session label: ", "", line)
elif line.startswith("media file size"):
out["Media size"] = re.sub(r"^media file size\s+", "", line)
elif line.startswith("dump complete:"):
out["Dump complete"] = re.sub(r"^dump complete:\s+", "", line)
elif line.startswith("Dump Status:"):
out["Status"] = re.sub(r"^Dump Status:\s+", "", line)
elif line.startswith("Dump Summary:"):
summary_block = True
continue
if line.startswith(" ") and summary_block:
summary.append(line.strip())
elif not line.startswith(" ") and summary_block:
summary_block = False
if summary:
out["Summary"] = " ".join(summary)
return out
def dump(device, destination, level=0, label=None, noerase=None):
"""
Dump filesystem device to the media (file, tape etc).
Required parameters:
* **device**: XFS device, content of which to be dumped.
* **destination**: Specifies a dump destination.
Valid options are:
* **label**: Label of the dump. Otherwise automatically generated label is used.
* **level**: Specifies a dump level of 0 to 9.
* **noerase**: Pre-erase media.
Other options are not used in order to let ``xfsdump`` use its default
values, as they are most optimal. See the ``xfsdump(8)`` manpage for
a more complete description of these options.
CLI Example:
.. code-block:: bash
salt '*' xfs.dump /dev/sda1 /detination/on/the/client
salt '*' xfs.dump /dev/sda1 /detination/on/the/client label='Company accountancy'
salt '*' xfs.dump /dev/sda1 /detination/on/the/client noerase=True
"""
if not salt.utils.path.which("xfsdump"):
raise CommandExecutionError('Utility "xfsdump" has to be installed or missing.')
label = (
label
and label
or time.strftime(
f'XFS dump for "{device}" of %Y.%m.%d, %H:%M', time.localtime()
).replace("'", '"')
)
cmd = ["xfsdump"]
cmd.append("-F") # Force
if not noerase:
cmd.append("-E") # pre-erase
cmd.append(f"-L '{label}'") # Label
cmd.append(f"-l {level}") # Dump level
cmd.append(f"-f {destination}") # Media destination
cmd.append(device) # Device
cmd = " ".join(cmd)
out = __salt__["cmd.run_all"](cmd)
_verify_run(out, cmd=cmd)
return _xfsdump_output(out["stdout"])
def _xr_to_keyset(line):
"""
Parse xfsrestore output keyset elements.
"""
tkns = [elm for elm in line.strip().split(":", 1) if elm]
if len(tkns) == 1:
return f"'{tkns[0]}': "
else:
key, val = tkns
return f"'{key.strip()}': '{val.strip()}',"
def _xfs_inventory_output(out):
"""
Transform xfsrestore inventory data output to a Python dict source and evaluate it.
"""
data = []
out = [line for line in out.split("\n") if line.strip()]
# No inventory yet
if len(out) == 1 and "restore status" in out[0].lower():
return {"restore_status": out[0]}
ident = 0
data.append("{")
for line in out[:-1]:
if len([elm for elm in line.strip().split(":") if elm]) == 1:
n_ident = len(re.sub("[^\t]", "", line))
if ident > n_ident:
for step in range(ident):
data.append("},")
ident = n_ident
data.append(_xr_to_keyset(line))
data.append("{")
else:
data.append(_xr_to_keyset(line))
for step in range(ident + 1):
data.append("},")
data.append("},")
# We are evaling into a python dict, a json load
# would be safer
data = eval("\n".join(data))[0] # pylint: disable=W0123
data["restore_status"] = out[-1]
return data
def inventory():
"""
Display XFS dump inventory without restoration.
CLI Example:
.. code-block:: bash
salt '*' xfs.inventory
"""
out = __salt__["cmd.run_all"]("xfsrestore -I")
_verify_run(out)
return _xfs_inventory_output(out["stdout"])
def _xfs_prune_output(out, uuid):
"""
Parse prune output.
"""
data = {}
cnt = []
cutpoint = False
for line in [l.strip() for l in out.split("\n") if l]:
if line.startswith("-"):
if cutpoint:
break
else:
cutpoint = True
continue
if cutpoint:
cnt.append(line)
for kset in [e for e in cnt[1:] if ":" in e]:
key, val = (t.strip() for t in kset.split(":", 1))
data[key.lower().replace(" ", "_")] = val
return data.get("uuid") == uuid and data or {}
def prune_dump(sessionid):
"""
Prunes the dump session identified by the given session id.
CLI Example:
.. code-block:: bash
salt '*' xfs.prune_dump b74a3586-e52e-4a4a-8775-c3334fa8ea2c
"""
out = __salt__["cmd.run_all"](f"xfsinvutil -s {sessionid} -F")
_verify_run(out)
data = _xfs_prune_output(out["stdout"], sessionid)
if data:
return data
raise CommandExecutionError(f'Session UUID "{sessionid}" was not found.')
def _blkid_output(out):
"""
Parse blkid output.
"""
def flt(data):
return [el for el in data if el.strip()]
data = {}
for dev_meta in flt(out.split("\n\n")):
dev = {}
for items in flt(dev_meta.strip().split("\n")):
key, val = items.split("=", 1)
dev[key.lower()] = val
if dev.pop("type", None) == "xfs":
dev["label"] = dev.get("label")
data[dev.pop("devname")] = dev
mounts = _get_mounts()
for device in mounts:
if data.get(device):
data[device].update(mounts[device])
return data
def devices():
"""
Get known XFS formatted devices on the system.
CLI Example:
.. code-block:: bash
salt '*' xfs.devices
"""
out = __salt__["cmd.run_all"]("blkid -o export")
_verify_run(out)
return _blkid_output(out["stdout"])
def _xfs_estimate_output(out):
"""
Parse xfs_estimate output.
"""
spc = re.compile(r"\s+")
data = {}
for line in [l for l in out.split("\n") if l.strip()][1:]:
directory, bsize, blocks, megabytes, logsize = spc.sub(" ", line).split(" ")
data[directory] = {
"block _size": bsize,
"blocks": blocks,
"megabytes": megabytes,
"logsize": logsize,
}
return data
def estimate(path):
"""
Estimate the space that an XFS filesystem will take.
For each directory estimate the space that directory would take
if it were copied to an XFS filesystem.
Estimation does not cross mount points.
CLI Example:
.. code-block:: bash
salt '*' xfs.estimate /path/to/file
salt '*' xfs.estimate /path/to/dir/*
"""
if not os.path.exists(path):
raise CommandExecutionError(f'Path "{path}" was not found.')
out = __salt__["cmd.run_all"](f"xfs_estimate -v {path}")
_verify_run(out)
return _xfs_estimate_output(out["stdout"])
def mkfs(
device,
label=None,
ssize=None,
noforce=None,
bso=None,
gmo=None,
ino=None,
lso=None,
rso=None,
nmo=None,
dso=None,
):
"""
Create a file system on the specified device. By default wipes out with force.
General options:
* **label**: Specify volume label.
* **ssize**: Specify the fundamental sector size of the filesystem.
* **noforce**: Do not force create filesystem, if disk is already formatted.
Filesystem geometry options:
* **bso**: Block size options.
* **gmo**: Global metadata options.
* **dso**: Data section options. These options specify the location, size,
and other parameters of the data section of the filesystem.
* **ino**: Inode options to specify the inode size of the filesystem, and other inode allocation parameters.
* **lso**: Log section options.
* **nmo**: Naming options.
* **rso**: Realtime section options.
See the ``mkfs.xfs(8)`` manpage for a more complete description of corresponding options description.
CLI Example:
.. code-block:: bash
salt '*' xfs.mkfs /dev/sda1
salt '*' xfs.mkfs /dev/sda1 dso='su=32k,sw=6' noforce=True
salt '*' xfs.mkfs /dev/sda1 dso='su=32k,sw=6' lso='logdev=/dev/sda2,size=10000b'
"""
def getopts(args):
return dict(
(args and ("=" in args) and args or None)
and [kw.split("=") for kw in args.split(",")]
or []
)
cmd = ["mkfs.xfs"]
if label:
cmd.append("-L")
cmd.append(f"'{label}'")
if ssize:
cmd.append("-s")
cmd.append(ssize)
for switch, opts in [
("-b", bso),
("-m", gmo),
("-n", nmo),
("-i", ino),
("-d", dso),
("-l", lso),
("-r", rso),
]:
try:
if getopts(opts):
cmd.append(switch)
cmd.append(opts)
except Exception: # pylint: disable=broad-except
raise CommandExecutionError(
f'Wrong parameters "{opts}" for option "{switch}"'
)
if not noforce:
cmd.append("-f")
cmd.append(device)
cmd = " ".join(cmd)
out = __salt__["cmd.run_all"](cmd)
_verify_run(out, cmd=cmd)
return _parse_xfs_info(out["stdout"])
def modify(device, label=None, lazy_counting=None, uuid=None):
"""
Modify parameters of an XFS filesystem.
CLI Example:
.. code-block:: bash
salt '*' xfs.modify /dev/sda1 label='My backup' lazy_counting=False
salt '*' xfs.modify /dev/sda1 uuid=False
salt '*' xfs.modify /dev/sda1 uuid=True
"""
if not label and lazy_counting is None and uuid is None:
raise CommandExecutionError(
f'Nothing specified for modification for "{device}" device'
)
cmd = ["xfs_admin"]
if label:
cmd.append("-L")
cmd.append(f"'{label}'")
if lazy_counting is False:
cmd.append("-c")
cmd.append("0")
elif lazy_counting:
cmd.append("-c")
cmd.append("1")
if uuid is False:
cmd.append("-U")
cmd.append("nil")
elif uuid:
cmd.append("-U")
cmd.append("generate")
cmd.append(device)
cmd = " ".join(cmd)
_verify_run(__salt__["cmd.run_all"](cmd), cmd=cmd)
out = __salt__["cmd.run_all"](f"blkid -o export {device}")
_verify_run(out)
return _blkid_output(out["stdout"])
def _get_mounts():
"""
List mounted filesystems.
"""
mounts = {}
with salt.utils.files.fopen("/proc/mounts") as fhr:
for line in salt.utils.data.decode(fhr.readlines()):
device, mntpnt, fstype, options, fs_freq, fs_passno = line.strip().split(
" "
)
if fstype != "xfs":
continue
mounts[device] = {
"mount_point": mntpnt,
"options": options.split(","),
}
return mounts
def defragment(device):
"""
Defragment mounted XFS filesystem.
In order to mount a filesystem, device should be properly mounted and writable.
CLI Example:
.. code-block:: bash
salt '*' xfs.defragment /dev/sda1
"""
if device == "/":
raise CommandExecutionError("Root is not a device.")
if not _get_mounts().get(device):
raise CommandExecutionError(f'Device "{device}" is not mounted')
out = __salt__["cmd.run_all"](f"xfs_fsr {device}")
_verify_run(out)
return {"log": out["stdout"]}
Zerion Mini Shell 1.0