Mini Shell
"""
Salt module to manage Unix cryptsetup jobs and the crypttab file
.. versionadded:: 2018.3.0
"""
# Import python libraries
import logging
import os
import re
# Import salt libraries
import salt.utils.files
import salt.utils.platform
import salt.utils.stringutils
from salt.exceptions import CommandExecutionError
# Set up logger
log = logging.getLogger(__name__)
# Define the module's virtual name
__virtualname__ = "cryptdev"
def __virtual__():
"""
Only load on POSIX-like systems
"""
if salt.utils.platform.is_windows():
return (False, "The cryptdev module cannot be loaded: not a POSIX-like system")
return True
class _crypttab_entry:
"""
Utility class for manipulating crypttab entries. Primarily we're parsing,
formatting, and comparing lines. Parsing emits dicts expected from
crypttab() or raises a ValueError.
"""
class ParseError(ValueError):
"""Error raised when a line isn't parsible as a crypttab entry"""
crypttab_keys = ("name", "device", "password", "options")
crypttab_format = "{name: <12} {device: <44} {password: <22} {options}\n"
@classmethod
def dict_from_line(cls, line, keys=crypttab_keys):
if len(keys) != 4:
raise ValueError(f"Invalid key array: {keys}")
if line.startswith("#"):
raise cls.ParseError("Comment!")
comps = line.split()
# If there are only three entries, then the options have been omitted.
if len(comps) == 3:
comps += [""]
if len(comps) != 4:
raise cls.ParseError("Invalid Entry!")
return dict(zip(keys, comps))
@classmethod
def from_line(cls, *args, **kwargs):
return cls(**cls.dict_from_line(*args, **kwargs))
@classmethod
def dict_to_line(cls, entry):
return cls.crypttab_format.format(**entry)
def __str__(self):
"""String value, only works for full repr"""
return self.dict_to_line(self.criteria)
def __repr__(self):
"""Always works"""
return repr(self.criteria)
def pick(self, keys):
"""Returns an instance with just those keys"""
subset = {key: self.criteria[key] for key in keys}
return self.__class__(**subset)
def __init__(self, **criteria):
"""Store non-empty, non-null values to use as filter"""
self.criteria = {
key: salt.utils.stringutils.to_unicode(value)
for key, value in criteria.items()
if value is not None
}
@staticmethod
def norm_path(path):
"""Resolve equivalent paths equivalently"""
return os.path.normcase(os.path.normpath(path))
def match(self, line):
"""Compare potentially partial criteria against a complete line"""
entry = self.dict_from_line(line)
for key, value in self.criteria.items():
if entry[key] != value:
return False
return True
def active():
"""
List existing device-mapper device details.
"""
ret = {}
# TODO: This command should be extended to collect more information, such as UUID.
devices = __salt__["cmd.run_stdout"]("dmsetup ls --target crypt")
out_regex = re.compile(r"(?P<devname>\S+)\s+\((?P<major>\d+), (?P<minor>\d+)\)")
log.debug(devices)
for line in devices.split("\n"):
match = out_regex.match(line)
if match:
dev_info = match.groupdict()
ret[dev_info["devname"]] = dev_info
else:
log.warning("dmsetup output does not match expected format")
return ret
def crypttab(config="/etc/crypttab"):
"""
List the contents of the crypttab
CLI Example:
.. code-block:: bash
salt '*' cryptdev.crypttab
"""
ret = {}
if not os.path.isfile(config):
return ret
with salt.utils.files.fopen(config) as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line).rstrip("\n")
try:
entry = _crypttab_entry.dict_from_line(line)
entry["options"] = entry["options"].split(",")
# Handle duplicate names by appending `_`
while entry["name"] in ret:
entry["name"] += "_"
ret[entry.pop("name")] = entry
except _crypttab_entry.ParseError:
pass
return ret
def rm_crypttab(name, config="/etc/crypttab"):
"""
Remove the named mapping from the crypttab. If the described entry does not
exist, nothing is changed, but the command succeeds by returning
``'absent'``. If a line is removed, it returns ``'change'``.
CLI Example:
.. code-block:: bash
salt '*' cryptdev.rm_crypttab foo
"""
modified = False
criteria = _crypttab_entry(name=name)
# For each line in the config that does not match the criteria, add it to
# the list. At the end, re-create the config from just those lines.
lines = []
try:
with salt.utils.files.fopen(config, "r") as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line)
try:
if criteria.match(line):
modified = True
else:
lines.append(line)
except _crypttab_entry.ParseError:
lines.append(line)
except OSError as exc:
msg = "Could not read from {0}: {1}"
raise CommandExecutionError(msg.format(config, exc))
if modified:
try:
with salt.utils.files.fopen(config, "w+") as ofile:
ofile.writelines(salt.utils.stringutils.to_str(line) for line in lines)
except OSError as exc:
msg = "Could not write to {0}: {1}"
raise CommandExecutionError(msg.format(config, exc))
# If we reach this point, the changes were successful
return "change" if modified else "absent"
def set_crypttab(
name,
device,
password="none",
options="",
config="/etc/crypttab",
test=False,
match_on="name",
):
"""
Verify that this device is represented in the crypttab, change the device to
match the name passed, or add the name if it is not present.
CLI Example:
.. code-block:: bash
salt '*' cryptdev.set_crypttab foo /dev/sdz1 mypassword swap,size=256
"""
# Fix the options type if it is not a string
if options is None:
options = ""
elif isinstance(options, str):
pass
elif isinstance(options, list):
options = ",".join(options)
else:
msg = "options must be a string or list of strings"
raise CommandExecutionError(msg)
# preserve arguments for updating
entry_args = {
"name": name,
"device": device,
"password": password if password is not None else "none",
"options": options,
}
lines = []
ret = None
# Transform match_on into list--items will be checked later
if isinstance(match_on, list):
pass
elif not isinstance(match_on, str):
msg = "match_on must be a string or list of strings"
raise CommandExecutionError(msg)
else:
match_on = [match_on]
# generate entry and criteria objects, handle invalid keys in match_on
entry = _crypttab_entry(**entry_args)
try:
criteria = entry.pick(match_on)
except KeyError:
def filterFn(key):
return key not in _crypttab_entry.crypttab_keys
invalid_keys = filter(filterFn, match_on)
msg = f'Unrecognized keys in match_on: "{invalid_keys}"'
raise CommandExecutionError(msg)
# parse file, use ret to cache status
if not os.path.isfile(config):
raise CommandExecutionError(f'Bad config file "{config}"')
try:
with salt.utils.files.fopen(config, "r") as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line)
try:
if criteria.match(line):
# Note: If ret isn't None here,
# we've matched multiple lines
ret = "present"
if entry.match(line):
lines.append(line)
else:
ret = "change"
lines.append(str(entry))
else:
lines.append(line)
except _crypttab_entry.ParseError:
lines.append(line)
except OSError as exc:
msg = "Couldn't read from {0}: {1}"
raise CommandExecutionError(msg.format(config, exc))
# add line if not present or changed
if ret is None:
lines.append(str(entry))
ret = "new"
if ret != "present": # ret in ['new', 'change']:
if not test:
try:
with salt.utils.files.fopen(config, "w+") as ofile:
# The line was changed, commit it!
ofile.writelines(
salt.utils.stringutils.to_str(line) for line in lines
)
except OSError:
msg = "File not writable {0}"
raise CommandExecutionError(msg.format(config))
return ret
def open(name, device, keyfile):
"""
Open a crypt device using ``cryptsetup``. The ``keyfile`` must not be
``None`` or ``'none'``, because ``cryptsetup`` will otherwise ask for the
password interactively.
CLI Example:
.. code-block:: bash
salt '*' cryptdev.open foo /dev/sdz1 /path/to/keyfile
"""
if keyfile is None or keyfile == "none" or keyfile == "-":
raise CommandExecutionError(
"For immediate crypt device mapping, keyfile must not be none"
)
code = __salt__["cmd.retcode"](
f"cryptsetup open --key-file {keyfile} {device} {name}"
)
return code == 0
def close(name):
"""
Close a crypt device using ``cryptsetup``.
CLI Example:
.. code-block:: bash
salt '*' cryptdev.close foo
"""
code = __salt__["cmd.retcode"](f"cryptsetup close {name}")
return code == 0
Zerion Mini Shell 1.0