File: //usr/lib/python2.7/site-packages/salt/modules/cryptdev.py
# -*- coding: utf-8 -*-
'''
Salt module to manage Unix cryptsetup jobs and the crypttab file
.. versionadded:: 2018.3.0
'''
# Import python libraries
from __future__ import absolute_import, print_function, unicode_literals
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
# Import 3rd-party libs
from salt.ext import six
# 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(object):
'''
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('Invalid key array: {0}'.format(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(six.moves.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 = dict([(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 six.iteritems(criteria)
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 six.iteritems(self.criteria):
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>\w+)\W+\((?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 (IOError, 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 (IOError, 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, six.string_types):
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, six.string_types):
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:
filterFn = lambda key: key not in _crypttab_entry.crypttab_keys
invalid_keys = six.moves.filter(filterFn, match_on)
msg = 'Unrecognized keys in match_on: "{0}"'.format(invalid_keys)
raise CommandExecutionError(msg)
# parse file, use ret to cache status
if not os.path.isfile(config):
raise CommandExecutionError('Bad config file "{0}"'.format(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(six.text_type(entry))
else:
lines.append(line)
except _crypttab_entry.ParseError:
lines.append(line)
except (IOError, 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(six.text_type(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 (IOError, 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']('cryptsetup open --key-file {0} {1} {2}'
.format(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']('cryptsetup close {0}'.format(name))
return code == 0