Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2016.3 mount vfstab support #34283

Merged
merged 10 commits into from
Jun 28, 2016
256 changes: 252 additions & 4 deletions salt/modules/mount.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,83 @@ def match(self, line):
return True


class _vfstab_entry(object):
'''
Utility class for manipulating vfstab 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'
Note: This parses vfstab entries on Solaris like systems

#device device mount FS fsck mount mount
#to mount to fsck point type pass at boot options
#
/devices - /devices devfs - no -
'''

class ParseError(ValueError):
'''Error raised when a line isn't parsible as an fstab entry'''

vfstab_keys = ('device', 'device_fsck', 'name', 'fstype', 'pass_fsck', 'mount_at_boot', 'opts')
## NOTE: weird formatting to match default spacing on Solaris
vfstab_format = '{device:<11} {device_fsck:<3} {name:<19} {fstype:<8} {pass_fsck:<3} {mount_at_boot:<6} {opts}\n'

@classmethod
def dict_from_line(cls, line):
if line.startswith('#'):
raise cls.ParseError("Comment!")

comps = line.split()
if len(comps) != 7:
raise cls.ParseError("Invalid Entry!")

return dict(zip(cls.vfstab_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.vfstab_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 str(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], str(key_value1[1])) for key_value1 in items]
self.criteria = dict(items)

@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 line'''
entry = self.dict_from_line(line)
for key, value in six.iteritems(self.criteria):
if entry[key] != value:
return False
return True


def fstab(config='/etc/fstab'):
'''
.. versionchanged:: 2016.3.2
List the contents of the fstab

CLI Example:
Expand All @@ -308,9 +383,16 @@ def fstab(config='/etc/fstab'):
with salt.utils.fopen(config) as ifile:
for line in ifile:
try:
entry = _fstab_entry.dict_from_line(
line,
_fstab_entry.compatibility_keys)
if __grains__['kernel'] == 'SunOS':
## Note: comments use in default vfstab file!
if line[0] == '#':
continue
entry = _vfstab_entry.dict_from_line(
line)
else:
entry = _fstab_entry.dict_from_line(
line,
_fstab_entry.compatibility_keys)

entry['opts'] = entry['opts'].split(',')
while entry['name'] in ret:
Expand All @@ -319,12 +401,30 @@ def fstab(config='/etc/fstab'):
ret[entry.pop('name')] = entry
except _fstab_entry.ParseError:
pass
except _vfstab_entry.ParseError:
pass

return ret


def vfstab(config='/etc/vfstab'):
'''
.. versionadded:: 2016.3.2
List the contents of the vfstab

CLI Example:

.. code-block:: bash

salt '*' mount.vfstab
'''
## NOTE: vfstab is a wrapper for fstab
return fstab(config)


def rm_fstab(name, device, config='/etc/fstab'):
'''
.. versionchanged:: 2016.3.2
Remove the mount point from the fstab

CLI Example:
Expand All @@ -335,7 +435,10 @@ def rm_fstab(name, device, config='/etc/fstab'):
'''
modified = False

criteria = _fstab_entry(name=name, device=device)
if __grains__['kernel'] == 'SunOS':
criteria = _vfstab_entry(name=name, device=device)
else:
criteria = _fstab_entry(name=name, device=device)

lines = []
try:
Expand All @@ -349,6 +452,8 @@ def rm_fstab(name, device, config='/etc/fstab'):

except _fstab_entry.ParseError:
lines.append(line)
except _vfstab_entry.ParseError:
lines.append(line)

except (IOError, OSError) as exc:
msg = "Couldn't read from {0}: {1}"
Expand All @@ -367,6 +472,21 @@ def rm_fstab(name, device, config='/etc/fstab'):
return True


def rm_vfstab(name, device, config='/etc/vfstab'):
'''
.. versionadded:: 2016.3.2
Remove the mount point from the vfstab

CLI Example:

.. code-block:: bash

salt '*' mount.rm_vfstab /mnt/foo /device/c0t0d0p0
'''
## NOTE: rm_vfstab is a wrapper for rm_fstab
return rm_fstab(name, device, config)


def set_fstab(
name,
device,
Expand Down Expand Up @@ -490,6 +610,134 @@ def set_fstab(
return ret


def set_vfstab(
name,
device,
fstype,
opts='-',
device_fsck='-',
pass_fsck='-',
mount_at_boot='yes',
config='/etc/vfstab',
test=False,
match_on='auto',
**kwargs):
'''
..verionadded:: 2016.3.2
Verify that this mount is represented in the fstab, 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_vfstab /mnt/foo /device/c0t0d0p0 ufs
'''

# Fix the opts type if it is a list
if isinstance(opts, list):
opts = ','.join(opts)

# Map unknown values for mount_at_boot to no
if mount_at_boot != 'yes':
mount_at_boot = 'no'

# preserve arguments for updating
entry_args = {
'name': name,
'device': device,
'fstype': fstype,
'opts': opts,
'device_fsck': device_fsck,
'pass_fsck': pass_fsck,
'mount_at_boot': mount_at_boot,
}

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)
elif match_on == 'auto':
# Try to guess right criteria for auto....
# NOTE: missing some special fstypes here
specialFSes = frozenset([
'devfs',
'proc',
'ctfs',
'objfs',
'sharefs',
'fs',
'tmpfs'])

if fstype in specialFSes:
match_on = ['name']
else:
match_on = ['device']
else:
match_on = [match_on]

# generate entry and criteria objects, handle invalid keys in match_on
entry = _vfstab_entry(**entry_args)
try:
criteria = entry.pick(match_on)

except KeyError:
filterFn = lambda key: key not in _vfstab_entry.vfstab_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))

try:
with salt.utils.fopen(config, 'r') as ifile:
for line in ifile:
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 _vfstab_entry.ParseError:
lines.append(line)

except (IOError, OSError) as exc:
msg = 'Couldn\'t read from {0}: {1}'
raise CommandExecutionError(msg.format(config, str(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 salt.utils.test_mode(test=test, **kwargs):
try:
with salt.utils.fopen(config, 'w+') as ofile:
# The line was changed, commit it!
ofile.writelines(lines)
except (IOError, OSError):
msg = 'File not writable {0}'
raise CommandExecutionError(msg.format(config))

return ret


def rm_automaster(name, device, config='/etc/auto_salt'):
'''
Remove the mount point from the auto_master
Expand Down
67 changes: 47 additions & 20 deletions tests/unit/modules/mount_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,35 +89,62 @@ def test_fstab(self):
self.assertEqual(mount.fstab(), {})

mock = MagicMock(return_value=True)
with patch.dict(mount.__grains__, {'kernel': ''}):
with patch.object(os.path, 'isfile', mock):
file_data = '\n'.join(['#',
'A B C D,E,F G H'])
with patch('salt.utils.fopen',
mock_open(read_data=file_data),
create=True) as m:
m.return_value.__iter__.return_value = file_data.splitlines()
self.assertEqual(mount.fstab(), {'B': {'device': 'A',
'dump': 'G',
'fstype': 'C',
'opts': ['D', 'E', 'F'],
'pass': 'H'}})

def test_vfstab(self):
'''
List the content of the vfstab
'''
mock = MagicMock(return_value=False)
with patch.object(os.path, 'isfile', mock):
file_data = '\n'.join(['#',
'A B C D,E,F G H'])
with patch('salt.utils.fopen',
mock_open(read_data=file_data),
create=True) as m:
m.return_value.__iter__.return_value = file_data.splitlines()
self.assertEqual(mount.fstab(), {'B': {'device': 'A',
'dump': 'G',
'fstype': 'C',
'opts': ['D', 'E', 'F'],
'pass': 'H'}})
self.assertEqual(mount.vfstab(), {})

mock = MagicMock(return_value=True)
with patch.dict(mount.__grains__, {'kernel': 'SunOS'}):
with patch.object(os.path, 'isfile', mock):
file_data = '\n'.join(['#',
'swap - /tmp tmpfs - yes size=2048m'])
with patch('salt.utils.fopen',
mock_open(read_data=file_data),
create=True) as m:
m.return_value.__iter__.return_value = file_data.splitlines()
self.assertEqual(mount.fstab(), {'/tmp': {'device': 'swap',
'device_fsck': '-',
'fstype': 'tmpfs',
'mount_at_boot': 'yes',
'opts': ['size=2048m'],
'pass_fsck': '-'}})

def test_rm_fstab(self):
'''
Remove the mount point from the fstab
'''
mock_fstab = MagicMock(return_value={})
with patch.object(mount, 'fstab', mock_fstab):
with patch('salt.utils.fopen', mock_open()):
self.assertTrue(mount.rm_fstab('name', 'device'))
with patch.dict(mount.__grains__, {'kernel': ''}):
with patch.object(mount, 'fstab', mock_fstab):
with patch('salt.utils.fopen', mock_open()):
self.assertTrue(mount.rm_fstab('name', 'device'))

mock_fstab = MagicMock(return_value={'name': 'name'})
with patch.object(mount, 'fstab', mock_fstab):
with patch('salt.utils.fopen', mock_open()) as m_open:
helper_open = m_open()
helper_open.write.assertRaises(CommandExecutionError,
mount.rm_fstab,
config=None)
with patch.dict(mount.__grains__, {'kernel': ''}):
with patch.object(mount, 'fstab', mock_fstab):
with patch('salt.utils.fopen', mock_open()) as m_open:
helper_open = m_open()
helper_open.write.assertRaises(CommandExecutionError,
mount.rm_fstab,
config=None)

def test_set_fstab(self):
'''
Expand Down