Merge pull request #20219 from rallytime/backport_19829

Backport #19829 to 2015.2
This commit is contained in:
Nicole Thomas 2015-01-29 15:24:56 -07:00
commit 6e8e3917b3
2 changed files with 192 additions and 125 deletions

View File

@ -191,6 +191,79 @@ def active(extended=False):
return ret return ret
class _fstab_entry(object):
'''
Utility class for manipulating fstab entries. Primarily we're parsing,
formating, 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'''
fstab_keys = ('device', 'name', 'fstype', 'opts', 'dump', 'pass_num')
# preserve data format
compatibility_keys = ('device', 'name', 'fstype', 'opts', 'dump', 'pass')
fstab_format = '{device}\t\t{name}\t{fstype}\t{opts}\t{dump} {pass_num}\n'
@classmethod
def dict_from_line(cls, line, keys=fstab_keys):
if len(keys) != 6:
raise ValueError('Invalid key array: {0}'.format(keys))
if line.startswith('#'):
raise cls.ParseError("Comment!")
comps = line.split()
if len(comps) != 6:
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.fstab_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(map(lambda key: (key, self.criteria[key]), keys))
return self.__class__(**subset)
def __init__(self, **criteria):
'''Store non-empty, non-null values to use as filter'''
items = filter(lambda (key, value): value is not None, criteria.items())
items = map(lambda (key, value): (key, str(value)), 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 self.criteria.items():
if entry[key] != value:
return False
return True
def fstab(config='/etc/fstab'): def fstab(config='/etc/fstab'):
''' '''
List the contents of the fstab List the contents of the fstab
@ -206,21 +279,16 @@ def fstab(config='/etc/fstab'):
return ret return ret
with salt.utils.fopen(config) as ifile: with salt.utils.fopen(config) as ifile:
for line in ifile: for line in ifile:
if line.startswith('#'): try:
# Commented entry = _fstab_entry.dict_from_line(
continue line,
if not line.strip(): _fstab_entry.compatibility_keys)
# Blank line
continue entry['opts'] = entry['opts'].split(',')
comps = line.split() ret[entry.pop('name')] = entry
if len(comps) != 6: except _fstab_entry.ParseError:
# Invalid entry pass
continue
ret[comps[1]] = {'device': comps[0],
'fstype': comps[2],
'opts': comps[3].split(','),
'dump': comps[4],
'pass': comps[5]}
return ret return ret
@ -234,45 +302,37 @@ def rm_fstab(name, device, config='/etc/fstab'):
salt '*' mount.rm_fstab /mnt/foo salt '*' mount.rm_fstab /mnt/foo
''' '''
contents = fstab(config) modified = False
if name not in contents:
return True criteria = _fstab_entry(name=name, device=device)
# The entry is present, get rid of it
lines = [] lines = []
try: try:
with salt.utils.fopen(config, 'r') as ifile: with salt.utils.fopen(config, 'r') as ifile:
for line in ifile: for line in ifile:
if line.startswith('#'): try:
# Commented if criteria.match(line):
modified = True
else:
lines.append(line)
except _fstab_entry.ParseError:
lines.append(line) lines.append(line)
continue
if not line.strip():
# Blank line
lines.append(line)
continue
comps = line.split()
if len(comps) != 6:
# Invalid entry
lines.append(line)
continue
comps = line.split()
if device:
if comps[1] == name and comps[0] == device:
continue
else:
if comps[1] == name:
continue
lines.append(line)
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
msg = "Couldn't read from {0}: {1}" msg = "Couldn't read from {0}: {1}"
raise CommandExecutionError(msg.format(config, str(exc))) raise CommandExecutionError(msg.format(config, str(exc)))
try: if modified:
with salt.utils.fopen(config, 'w+') as ofile: try:
ofile.writelines(lines) with salt.utils.fopen(config, 'w+') as ofile:
except (IOError, OSError) as exc: ofile.writelines(lines)
msg = "Couldn't write to {0}: {1}" except (IOError, OSError) as exc:
raise CommandExecutionError(msg.format(config, str(exc))) msg = "Couldn't write to {0}: {1}"
raise CommandExecutionError(msg.format(config, str(exc)))
# Note: not clear why we always return 'True'
# --just copying previous behavior at this point...
return True return True
@ -285,6 +345,7 @@ def set_fstab(
pass_num=0, pass_num=0,
config='/etc/fstab', config='/etc/fstab',
test=False, test=False,
match_on='auto',
**kwargs): **kwargs):
''' '''
Verify that this mount is represented in the fstab, change the mount Verify that this mount is represented in the fstab, change the mount
@ -296,66 +357,89 @@ def set_fstab(
salt '*' mount.set_fstab /mnt/foo /dev/sdz1 ext4 salt '*' mount.set_fstab /mnt/foo /dev/sdz1 ext4
''' '''
# Fix the opts type if it is a list # Fix the opts type if it is a list
if isinstance(opts, list): if isinstance(opts, list):
opts = ','.join(opts) opts = ','.join(opts)
lines = []
change = False
present = False
# preserve arguments for updating
entry_args = dict(vars())
map(entry_args.pop, ['test', 'config', 'match_on', 'kwargs'])
lines = []
ret = None
# Transform match_on into list--items will be checked later
if isinstance(match_on, list):
pass
elif not isinstance(match_on, 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([
'tmpfs',
'sysfs',
'proc',
'fusectl',
'debugfs',
'securityfs',
'devtmpfs',
'cgroup'])
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 = _fstab_entry(**entry_args)
try:
criteria = entry.pick(match_on)
except KeyError:
filterFn = lambda key: key not in _fstab_entry.fstab_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): if not os.path.isfile(config):
raise CommandExecutionError('Bad config file "{0}"'.format(config)) raise CommandExecutionError('Bad config file "{0}"'.format(config))
try: try:
with salt.utils.fopen(config, 'r') as ifile: with salt.utils.fopen(config, 'r') as ifile:
for line in ifile: for line in ifile:
if line.startswith('#'): try:
# Commented if criteria.match(line):
lines.append(line) # Note: If ret isn't None here,
continue # we've matched multiple lines
if not line.strip(): ret = 'present'
# Blank line if entry.match(line):
lines.append(line) lines.append(line)
continue else:
comps = line.split() ret = 'change'
if len(comps) != 6: lines.append(str(entry))
# Invalid entry else:
lines.append(line) lines.append(line)
continue
if comps[1] == name and comps[0] == device: except _fstab_entry.ParseError:
# check to see if there are changes
# and fix them if there are any
present = True
if comps[2] != fstype:
change = True
comps[2] = fstype
if comps[3] != opts:
change = True
comps[3] = opts
if comps[4] != str(dump):
change = True
comps[4] = str(dump)
if comps[5] != str(pass_num):
change = True
comps[5] = str(pass_num)
if change:
log.debug(
'fstab entry for mount point {0} needs to be '
'updated'.format(name)
)
newline = (
'{0}\t\t{1}\t{2}\t{3}\t{4} {5}\n'.format(
device, name, fstype, opts, dump, pass_num
)
)
lines.append(newline)
else:
lines.append(line) lines.append(line)
except (IOError, OSError) as exc: except (IOError, OSError) as exc:
msg = 'Couldn\'t read from {0}: {1}' msg = 'Couldn\'t read from {0}: {1}'
raise CommandExecutionError(msg.format(config, str(exc))) raise CommandExecutionError(msg.format(config, str(exc)))
if change: # 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): if not salt.utils.test_mode(test=test, **kwargs):
try: try:
with salt.utils.fopen(config, 'w+') as ofile: with salt.utils.fopen(config, 'w+') as ofile:
@ -365,33 +449,7 @@ def set_fstab(
msg = 'File not writable {0}' msg = 'File not writable {0}'
raise CommandExecutionError(msg.format(config)) raise CommandExecutionError(msg.format(config))
return 'change' return ret
if not change:
if present:
# The right entry is already here
return 'present'
else:
if not salt.utils.test_mode(test=test, **kwargs):
# The entry is new, add it to the end of the fstab
newline = '{0}\t\t{1}\t{2}\t{3}\t{4} {5}\n'.format(device,
name,
fstype,
opts,
dump,
pass_num)
lines.append(newline)
try:
with salt.utils.fopen(config, 'w+') as ofile:
# The line was changed, commit it!
ofile.writelines(lines)
except (IOError, OSError):
raise CommandExecutionError(
'File not writable {0}'.format(
config
)
)
return 'new'
def rm_automaster(name, device, config='/etc/auto_salt'): def rm_automaster(name, device, config='/etc/auto_salt'):
@ -524,11 +582,11 @@ def set_automaster(
log.debug( log.debug(
'auto_master entry for mount point {0} needs to be ' 'auto_master entry for mount point {0} needs to be '
'updated'.format(name) 'updated'.format(name)
) )
newline = ( newline = (
'{0}\t{1}\t{2}\n'.format( '{0}\t{1}\t{2}\n'.format(
name, type_opts, device_fmt) name, type_opts, device_fmt)
) )
lines.append(newline) lines.append(newline)
else: else:
lines.append(line) lines.append(line)
@ -558,7 +616,7 @@ def set_automaster(
newline = ( newline = (
'{0}\t{1}\t{2}\n'.format( '{0}\t{1}\t{2}\n'.format(
name, type_opts, device_fmt) name, type_opts, device_fmt)
) )
lines.append(newline) lines.append(newline)
try: try:
with salt.utils.fopen(config, 'w+') as ofile: with salt.utils.fopen(config, 'w+') as ofile:
@ -568,8 +626,8 @@ def set_automaster(
raise CommandExecutionError( raise CommandExecutionError(
'File not writable {0}'.format( 'File not writable {0}'.format(
config config
) )
) )
return 'new' return 'new'

View File

@ -50,7 +50,8 @@ def mounted(name,
config='/etc/fstab', config='/etc/fstab',
persist=True, persist=True,
mount=True, mount=True,
user=None): user=None,
match_on='auto'):
''' '''
Verify that a device is mounted Verify that a device is mounted
@ -90,6 +91,12 @@ def mounted(name,
user user
The user to own the mount; this defaults to the user salt is The user to own the mount; this defaults to the user salt is
running as on the minion running as on the minion
match_on
A name or list of fstab properties on which this state should be applied.
Default is ``auto``, a special value indicating to guess based on fstype.
In general, ``auto`` matches on name for recognized special devices and
device otherwise.
''' '''
ret = {'name': name, ret = {'name': name,
'changes': {}, 'changes': {},
@ -319,7 +326,8 @@ def mounted(name,
dump, dump,
pass_num, pass_num,
config, config,
test=True) test=True,
match_on=match_on)
if out != 'present': if out != 'present':
ret['result'] = None ret['result'] = None
if out == 'new': if out == 'new':
@ -361,7 +369,8 @@ def mounted(name,
opts, opts,
dump, dump,
pass_num, pass_num,
config) config,
match_on=match_on)
if out == 'present': if out == 'present':
ret['comment'] += '. Entry already exists in the fstab.' ret['comment'] += '. Entry already exists in the fstab.'