salt.modules.ssh: big changes to clean things up

- Move all open(fname, ...).read()/.readlines() over to use
      try/except inside a with statement properly and raise a
      CommandExecutionError otherwise
    - Add __virtual__() so this module is never ran on windows
    - Get rid of the gratuitous use of for ... open(...).readlines():
      since file-like objects are iterable and doing that natively
      uses a lot less memory. This helps with some of the "leaks"
    - Fix the docstring in host_keys() and raise SaltInvocationError()
      on any non-Linux host and the user doesn't specify the key dir.
    - Add various TODO items for things like dead code

Overall... This module still needs a lot more love and some testing
This commit is contained in:
Jeff Schroeder 2012-11-11 21:41:09 -08:00
parent 2c5ac88cfc
commit 18d24ea907

View File

@ -3,8 +3,23 @@ Manage client ssh components
'''
import os
import re
import binascii
import hashlib
import binascii
import logging
import salt.utils
from salt.exceptions import (
SaltInvocationError,
CommandExecutionError,
)
log = logging.getLogger(__name__)
def __virtual__():
# TODO: This could work on windows with some love
if __grains__['os'] == 'Windows':
return False
return 'ssh'
def _refine_enc(enc):
@ -66,24 +81,35 @@ def _replace_auth_key(
lines = []
uinfo = __salt__['user.info'](user)
full = os.path.join(uinfo['home'], config)
for line in open(full, 'r').readlines():
if line.startswith('#'):
# Commented Line
lines.append(line)
continue
comps = line.split()
if len(comps) < 2:
# Not a valid line
lines.append(line)
continue
key_ind = 1
if comps[0][:4:] not in ['ssh-', 'ecds']:
key_ind = 2
if comps[key_ind] == key:
lines.append(auth_line)
else:
lines.append(line)
open(full, 'w+').writelines(lines)
try:
# open the file for both reading AND writing
with open(full, 'r') as _fh:
for line in _fh:
if line.startswith('#'):
# Commented Line
lines.append(line)
continue
comps = line.split()
if len(comps) < 2:
# Not a valid line
lines.append(line)
continue
key_ind = 1
if comps[0][:4:] not in ['ssh-', 'ecds']:
key_ind = 2
if comps[key_ind] == key:
lines.append(auth_line)
else:
lines.append(line)
_fh.close()
# Re-open the file writable after properly closing it
with open(full, 'w') as _fh:
# Write out any changes
_fh.writelines(lines)
except (IOError, OSError) as exc:
msg = 'Problem reading or writing to key file: {0}'
raise CommandExecutionError(msg.format(str(exc)))
def _validate_keys(key_file):
@ -92,44 +118,47 @@ def _validate_keys(key_file):
'''
ret = {}
linere = re.compile(r'^(.*?)\s?((?:ssh\-|ecds).+)$')
try:
for line in open(key_file, 'r').readlines():
if line.startswith('#'):
# Commented Line
continue
with open(key_file, 'r') as _fh:
for line in _fh:
if line.startswith('#'):
# Commented Line
continue
# get "{options} key"
ln = re.search(linere, line)
if not ln:
# not an auth ssh key, perhaps a blank line
continue
# get "{options} key"
ln = re.search(linere, line)
if not ln:
# not an auth ssh key, perhaps a blank line
continue
opts = ln.group(1)
comps = ln.group(2).split()
opts = ln.group(1)
comps = ln.group(2).split()
if len(comps) < 2:
# Not a valid line
continue
if len(comps) < 2:
# Not a valid line
continue
if opts:
# It has options, grab them
options = opts.split(',')
else:
options = []
if opts:
# It has options, grab them
options = opts.split(',')
else:
options = []
enc = comps[0]
key = comps[1]
comment = ' '.join(comps[2:])
fingerprint = _fingerprint(key)
if fingerprint is None:
continue
enc = comps[0]
key = comps[1]
comment = ' '.join(comps[2:])
fingerprint = _fingerprint(key)
if fingerprint is None:
continue
ret[key] = {'enc': enc,
'comment': comment,
'options': options,
'fingerprint': fingerprint}
except IOError:
return {}
ret[key] = {'enc': enc,
'comment': comment,
'options': options,
'fingerprint': fingerprint}
except (IOError, OSError) as exc:
msg = 'Problem reading ssh key file {0}'
raise CommandExecutionError(msg.format(key_file))
return ret
@ -160,11 +189,14 @@ def host_keys(keydir=None):
salt '*' ssh.host_keys
'''
# Set up the default keydir - needs to support sshd_config parsing in the
# future
# TODO: support parsing sshd_config for the key directory
if not keydir:
if __grains__['kernel'] == 'Linux':
keydir = '/etc/ssh'
else:
# If keydir is None, os.listdir() will blow up
msg = 'ssh.host_keys: Please specify a keydir'
raise SaltInvocationError(msg)
keys = {}
for fn_ in os.listdir(keydir):
if fn_.startswith('ssh_host_'):
@ -174,7 +206,8 @@ def host_keys(keydir=None):
if len(top) > 1:
kname += '.{0}'.format(top[1])
try:
keys[kname] = open(os.path.join(keydir, fn_), 'r').read()
with open(os.path.join(keydir, fn_), 'r') as _fh:
keys[kname] = _fh.readline().strip()
except (IOError, OSError):
keys[kname] = ''
return keys
@ -223,7 +256,7 @@ def check_key(user, key, enc, comment, options, config='.ssh/authorized_keys'):
CLI Example::
salt '*' ssh.check_key <user> <key>
salt '*' ssh.check_key <user> <key> <enc> <comment> <options>
'''
current = auth_keys(user, config)
nline = _format_auth_line(key, enc, comment, options)
@ -254,36 +287,53 @@ def rm_auth_key(user, key, config='.ssh/authorized_keys'):
# Remove the key
uinfo = __salt__['user.info'](user)
full = os.path.join(uinfo['home'], config)
# Return something sensible if the file doesn't exist
if not os.path.isfile(full):
return 'User authorized keys file not present'
return 'Authorized keys file {1} not present'.format(full)
lines = []
for line in open(full, 'r').readlines():
if line.startswith('#'):
# Commented Line
lines.append(line)
continue
try:
# Read every line in the file to find the right ssh key
# and then write out the correct one. Open the file once
with open(full, 'r') as _fh:
for line in _fh:
if line.startswith('#'):
# Commented Line
lines.append(line)
continue
# get "{options} key"
ln = re.search(linere, line)
if not ln:
# not an auth ssh key, perhaps a blank line
continue
# get "{options} key"
ln = re.search(linere, line)
if not ln:
# not an auth ssh key, perhaps a blank line
continue
comps = ln.group(2).split()
comps = ln.group(2).split()
if len(comps) < 2:
# Not a valid line
lines.append(line)
continue
if len(comps) < 2:
# Not a valid line
lines.append(line)
continue
pkey = comps[1]
pkey = comps[1]
if pkey == key:
continue
else:
lines.append(line)
open(full, 'w+').writelines(lines)
# This is the key we are "deleting", so don't put
# it in the list of keys to be re-added back
if pkey == key:
continue
lines.append(line)
# Let the context manager do the right thing here and then
# re-open the file in write mode to save the changes out.
with open(full, 'w') as _fh:
_fh.writelines(lines)
except (IOError, OSError) as exc:
log.warn('Could not read/write key file: {0}'.format(str(exc)))
return 'Key not removed'
return 'Key removed'
# TODO: Should this function return a simple boolean?
return 'Key not present'
def set_auth_key_from_file(
@ -302,7 +352,8 @@ def set_auth_key_from_file(
# TODO: add support for pulling keys from other file sources as well
lfile = __salt__['cp.cache_file'](source, env)
if not os.path.isfile(lfile):
return 'fail'
msg = 'Failed to pull key file from salt file server'
raise CommandExecutionError(msg)
newkey = {}
rval = ''
@ -380,12 +431,21 @@ def set_auth_key(
os.chmod(dpath, 448)
if not os.path.isfile(fconfig):
open(fconfig, 'a+').write('{0}'.format(auth_line))
new_file = True
else:
new_file = False
try:
with open(fconfig, 'a+') as _fh:
_fh.write('{0}'.format(auth_line))
except (IOError, OSError) as exc:
msg = 'Could not write to key file: {0}'
raise CommandExecutionError(msg.format(str(exc)))
if new_file:
if os.geteuid() == 0:
os.chown(fconfig, uinfo['uid'], uinfo['gid'])
os.chmod(fconfig, 384)
else:
open(fconfig, 'a+').write('{0}'.format(auth_line))
return 'new'
@ -429,7 +489,7 @@ def get_known_host(user, hostname, config='.ssh/known_hosts'):
def recv_known_host(user, hostname, enc=None, port=None, hash_hostname=False):
'''
Retreive information about host public key from remote server
Retrieve information about host public key from remote server
CLI Example::
@ -543,12 +603,18 @@ def set_known_host(user, hostname,
uinfo = __salt__['user.info'](user)
full = os.path.join(uinfo['home'], config)
line = '{hostname} {enc} {key}\n'.format(**remote_host)
with open(full, 'a') as fd:
fd.write(line)
try:
with open(full, 'a') as fd:
fd.write(line)
except (IOError, OSError) as exc:
raise CommandExecutionError("Couldn't append to known hosts file")
if os.geteuid() == 0:
os.chown(full, uinfo['uid'], uinfo['gid'])
return {'status': 'updated', 'old': stored_host, 'new': remote_host}
# TODO: The lines below this are dead code, fix the above return and make these work
status = check_known_host(user, hostname, fingerprint=fingerprint,
config=config)
if status == 'exists':