diff --git a/salt/modules/mount.py b/salt/modules/mount.py index 17f9fff44870..7e6e6fb0b6a2 100644 --- a/salt/modules/mount.py +++ b/salt/modules/mount.py @@ -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: @@ -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: @@ -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: @@ -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: @@ -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}" @@ -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, @@ -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 diff --git a/tests/unit/modules/mount_test.py b/tests/unit/modules/mount_test.py index dd837604a3ed..290c3680148a 100644 --- a/tests/unit/modules/mount_test.py +++ b/tests/unit/modules/mount_test.py @@ -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): '''