Initial support for handling /etc/filesystems on AIX

This commit is contained in:
David Murphy 2018-07-11 16:08:35 -06:00
parent b87bf905c2
commit 210076276f

View file

@ -5,6 +5,7 @@ Salt module to manage Unix mounts and the fstab file
# Import python libs
from __future__ import absolute_import, print_function, unicode_literals
from collections import OrderedDict
import os
import re
import logging
@ -23,6 +24,7 @@ from salt.exceptions import CommandNotFoundError, CommandExecutionError
from salt.ext import six
from salt.ext.six.moves import filter, zip # pylint: disable=import-error,redefined-builtin
# Set up logger
log = logging.getLogger(__name__)
@ -432,6 +434,138 @@ class _vfstab_entry(object):
return True
class _filesystems_entry(object):
'''
Utility class for manipulating filesystem entries. Primarily we're parsing,
formatting, and comparing lines. Parsing emits dicts expected from
fstab() or raises a ValueError.
Note: We'll probably want to use os.normpath and os.normcase on 'name'
'''
class ParseError(ValueError):
'''
Error raised when a line isn't parsible as an fstab entry
'''
filesystems_keys = ('device', 'name', 'fstype', 'vfstype', 'opts', 'mount')
# preserve data format
compatibility_keys = ('device', 'name', 'fstype', 'vfstype', 'opts', 'mount', 'account', 'boot', 'check', 'free', 'nodename', 'quota', 'size', 'vol', 'log')
@classmethod
def dict_from_lines(cls, lines, keys=filesystems_keys):
if len(lines) < 2:
raise ValueError('Invalid number of lines: {0}'.format(lines))
if len(keys) < 6:
raise ValueError('Invalid key array: {0}'.format(keys))
blk_lines = lines
orddict = OrderedDict()
orddict['name'] = blk_lines[0].split(':')[0].strip()
blk_lines.pop(0)
for line in blk_lines:
if line.startswith('#'):
raise cls.ParseError("Comment!")
comps = line.split('= ')
if len(comps) != 2:
raise cls.ParseError("Invalid Entry!")
orddict[comps[0].strip()] = comps[1].strip()
return orddict
@classmethod
def dict_from_cmd_line(cls, ipargs, keys=filesystems_keys):
if len(keys) < 1:
raise ValueError('Invalid key array: {0}'.format(keys))
cmdln_dict = ipargs
for key in keys:
# ignore unknown or local scope keys
if key.startswith('__'):
continue
if key not in _filesystems_entry.compatibility_keys:
cmdln_dict[key] = keys[key]
return cmdln_dict
@classmethod
def from_line(cls, *args, **kwargs):
return cls(** cls.dict_from_cmd_line(*args, **kwargs))
@classmethod
def dict_to_lines(cls, fsys_dict_entry):
entry = fsys_dict_entry
strg_out = entry['name'] + ':' + os.linesep
for k, v in six.viewitems(entry):
if 'name' not in k:
strg_out += '\t{0}\t\t= {1}'.format(k, v) + os.linesep
strg_out += os.linesep
return six.text_type(strg_out)
def dict_from_entry(self):
ret = OrderedDict()
ret[self.criteria['name']] = self.criteria
return ret
def __str__(self):
'''
String value, only works for full repr
'''
return self.dict_to_lines(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
'''
items = [key_value for key_value in six.iteritems(criteria) if key_value[1] is not None]
items = [(key_value1[0], six.text_type(key_value1[1])) for key_value1 in items]
self.criteria = OrderedDict(items)
@staticmethod
def norm_path(path):
'''
Resolve equivalent paths equivalently
'''
return os.path.normcase(os.path.normpath(path))
def match(self, fsys_view):
'''
Compare potentially partial criteria against built filesystems entry dictionary
'''
evalue_dict = fsys_view[1]
for key, value in six.viewitems(self.criteria):
if key in evalue_dict:
if evalue_dict[key] != value:
return False
else:
return False
return True
def __getitem__(self, key):
'''
Return value for input key
'''
return self.criteria[key]
def fstab(config='/etc/fstab'):
'''
.. versionchanged:: 2016.3.2
@ -1422,3 +1556,274 @@ def delete_mount_cache(real_name):
if not cache_write:
raise CommandExecutionError('Unable to write mount cache.')
return True
def _filesystems(config='/etc/filesystems', leading_key=True):
'''
.. versionadded:: 2018.3.3
List the contents of the filesystems
config
File containing filesystem infomation
leading_key
True return dictionary keyed by 'name' value and dictionary with other keys, values
{ '/dir' : { 'dev': '/dev/hd8', .... }}
False return dictionary keyed by 'name' value and dictionary with all keys, values
{ '/dir' : { 'name' : '/dir', 'dev': '/dev/hd8', .... }}
'''
ret = OrderedDict()
lines = []
parsing_block = False
if not os.path.isfile(config) or 'AIX' not in __grains__['kernel']:
return ret
# read in block of filesystems, block starts with '/' till empty line
with salt.utils.files.fopen(config) as ifile:
for line in ifile:
line = salt.utils.stringutils.to_unicode(line)
# skip till first entry
if not line.startswith('/') and not parsing_block:
continue
if line.startswith('/'):
parsing_block = True
lines.append(line)
elif not line.split():
parsing_block = False
try:
entry = _filesystems_entry.dict_from_lines(
lines,
_filesystems_entry.compatibility_keys)
lines = []
if 'opts' in entry:
entry['opts'] = entry['opts'].split(',')
while entry['name'] in ret:
entry['name'] += '_'
if leading_key:
ret[entry.pop('name')] = entry
else:
ret[entry['name']] = entry
except _filesystems_entry.ParseError:
pass
else:
lines.append(line)
return ret
def filesystems(config='/etc/filesystems'):
'''
.. versionadded:: 2018.3.3
List the contents of the filesystems
CLI Example:
.. code-block:: bash
salt '*' mount.filesystems
'''
ret = {}
if 'AIX' not in __grains__['kernel']:
return ret
return _filesystems(config)
def set_filesystems(
name,
device,
fstype,
vfstype,
opts='-',
mount='true',
config='/etc/filesystems',
test=False,
match_on='auto',
**kwargs):
'''
.. versionadded:: 2018.3.3
Verify that this mount is represented in the filesystems, change the mount
to match the data passed, or add the mount if it is not present.
CLI Example:
.. code-block:: bash
salt '*' mount.set_filesystems /mnt/foo /dev/sdz1 jfs2
'''
# Fix the opts type if it is a list
if isinstance(opts, list):
opts = ','.join(opts)
# preserve arguments for updating
entry_args = {
'name': name,
'dev': device.replace('\\ ', '\\040'),
'fstype': fstype,
'vfstype': vfstype,
'opts': opts,
'mount': mount,
}
view_lines = []
ret = None
if 'AIX' not in __grains__['kernel']:
return ret
# 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)
elif match_on == 'auto':
# Try to guess right criteria for auto....
# added IBM types from sys/vmount.h after btrfs
# NOTE: missing some special fstypes here
specialFSes = frozenset([
'none',
'tmpfs',
'sysfs',
'proc',
'fusectl',
'debugfs',
'securityfs',
'devtmpfs',
'cgroup',
'btrfs',
'cdrfs',
'procfs',
'jfs',
'jfs2',
'nfs',
'sfs',
'nfs3',
'cachefs',
'udfs',
'cifs',
'namefs',
'pmemfs',
'ahafs',
'nfs4',
'autofs',
'stnfs'])
if fstype in specialFSes:
match_on = ['name']
else:
match_on = ['dev']
else:
match_on = [match_on]
# generate entry and criteria objects, handle invalid keys in match_on
entry_ip = _filesystems_entry.from_line(entry_args, kwargs)
try:
criteria = entry_ip.pick(match_on)
except KeyError:
filterFn = lambda key: key not in _filesystems_entry.filesystems_keys
invalid_keys = 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))
# read in block of filesystem, block starts with '/' till empty line
try:
fsys_filedict = _filesystems(config, False)
for fsys_view in six.viewitems(fsys_filedict):
if criteria.match(fsys_view):
ret = 'present'
if entry_ip.match(fsys_view):
view_lines.append(fsys_view)
else:
ret = 'change'
kv = entry_ip['name']
view_lines.append((kv, entry_ip))
else:
view_lines.append(fsys_view)
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:
for dict_view in six.viewitems(entry_ip.dict_from_entry()):
view_lines.append(dict_view)
ret = 'new'
if ret != 'present': # ret in ['new', 'change']:
try:
with salt.utils.files.fopen(config, 'wb') as ofile:
# The line was changed, commit it!
for fsys_view in view_lines:
entry = fsys_view[1]
mystrg = _filesystems_entry.dict_to_lines(entry)
ofile.writelines(salt.utils.data.encode(mystrg))
except (IOError, OSError):
msg = 'File not writable {0}'
raise CommandExecutionError(msg.format(config))
return ret
def rm_filesystems(name, device, config='/etc/filesystems'):
'''
.. versionadded:: 2018.3.3
Remove the mount point from the filesystems
CLI Example:
.. code-block:: bash
salt '*' mount.rm_filesystems /mnt/foo /dev/sdg
'''
modified = False
view_lines = []
if 'AIX' not in __grains__['kernel']:
return modified
criteria = _filesystems_entry(name=name, dev=device)
try:
fsys_filedict = _filesystems(config, False)
for fsys_view in six.viewitems(fsys_filedict):
try:
if criteria.match(fsys_view):
modified = True
else:
view_lines.append(fsys_view)
except _filesystems_entry.ParseError:
view_lines.append(fsys_view)
except (IOError, OSError) as exc:
msg = "Couldn't read from {0}: {1}"
raise CommandExecutionError(msg.format(config, exc))
if modified:
try:
with salt.utils.files.fopen(config, 'wb') as ofile:
for fsys_view in view_lines:
entry = fsys_view[1]
mystrg = _filesystems_entry.dict_to_lines(entry)
ofile.writelines(salt.utils.data.encode(mystrg))
except (IOError, OSError) as exc:
msg = "Couldn't write to {0}: {1}"
raise CommandExecutionError(msg.format(config, exc))
return modified