Add host negation support to paramiko config.
This is a rewrite of the SSHConfig class to conform with the rules specified by the manpage for ssh_config. This change also adds support for negation according to the rules introduced by OpenSSH 5.9. Reference: http://www.openssh.com/txt/release-5.9
This commit is contained in:
parent
21689d9647
commit
57d776b318
|
@ -1,4 +1,5 @@
|
||||||
# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
|
# Copyright (C) 2006-2007 Robey Pointer <robeypointer@gmail.com>
|
||||||
|
# Copyright (C) 2012 Olle Lundberg <geek@nerd.sh>
|
||||||
#
|
#
|
||||||
# This file is part of paramiko.
|
# This file is part of paramiko.
|
||||||
#
|
#
|
||||||
|
@ -82,7 +83,8 @@ class SSHConfig (object):
|
||||||
"""
|
"""
|
||||||
Create a new OpenSSH config object.
|
Create a new OpenSSH config object.
|
||||||
"""
|
"""
|
||||||
self._config = [ { 'host': '*' } ]
|
self._config = []
|
||||||
|
|
||||||
|
|
||||||
def parse(self, file_obj):
|
def parse(self, file_obj):
|
||||||
"""
|
"""
|
||||||
|
@ -91,7 +93,7 @@ class SSHConfig (object):
|
||||||
@param file_obj: a file-like object to read the config file from
|
@param file_obj: a file-like object to read the config file from
|
||||||
@type file_obj: file
|
@type file_obj: file
|
||||||
"""
|
"""
|
||||||
configs = [self._config[0]]
|
host = {"host":['*'],"config":{}}
|
||||||
for line in file_obj:
|
for line in file_obj:
|
||||||
line = line.rstrip('\n').lstrip()
|
line = line.rstrip('\n').lstrip()
|
||||||
if (line == '') or (line[0] == '#'):
|
if (line == '') or (line[0] == '#'):
|
||||||
|
@ -115,20 +117,20 @@ class SSHConfig (object):
|
||||||
value = line[i:].lstrip()
|
value = line[i:].lstrip()
|
||||||
|
|
||||||
if key == 'host':
|
if key == 'host':
|
||||||
del configs[:]
|
self._config.append(host)
|
||||||
# the value may be multiple hosts, space-delimited
|
value = value.split()
|
||||||
for host in value.split():
|
host = {key:value,'config':{}}
|
||||||
# do we have a pre-existing host config to append to?
|
#identitifile is a special case, since it is allowed to be
|
||||||
matches = [c for c in self._config if c['host'] == host]
|
# specified multiple times and they should be tried in order
|
||||||
if len(matches) > 0:
|
# of specification.
|
||||||
configs.append(matches[0])
|
elif key == 'identityfile':
|
||||||
else:
|
if key in host['config']:
|
||||||
config = { 'host': host }
|
host['config']['identityfile'].append(value)
|
||||||
self._config.append(config)
|
else:
|
||||||
configs.append(config)
|
host['config']['identityfile'] = [value]
|
||||||
else:
|
elif key not in host['config']:
|
||||||
for config in configs:
|
host['config'].update({key:value})
|
||||||
config[key] = value
|
self._config.append(host)
|
||||||
|
|
||||||
def lookup(self, hostname):
|
def lookup(self, hostname):
|
||||||
"""
|
"""
|
||||||
|
@ -143,31 +145,45 @@ class SSHConfig (object):
|
||||||
will win out.
|
will win out.
|
||||||
|
|
||||||
The keys in the returned dict are all normalized to lowercase (look for
|
The keys in the returned dict are all normalized to lowercase (look for
|
||||||
C{"port"}, not C{"Port"}. No other processing is done to the keys or
|
C{"port"}, not C{"Port"}. The values are processed according to the
|
||||||
values.
|
rules for substitution variable expansion in C{ssh_config}.
|
||||||
|
|
||||||
@param hostname: the hostname to lookup
|
@param hostname: the hostname to lookup
|
||||||
@type hostname: str
|
@type hostname: str
|
||||||
"""
|
"""
|
||||||
matches = [x for x in self._config if fnmatch.fnmatch(hostname, x['host'])]
|
|
||||||
# Move * to the end
|
matches = [ config for config in self._config if
|
||||||
_star = matches.pop(0)
|
self._allowed(hostname,config['host']) ]
|
||||||
matches.append(_star)
|
|
||||||
ret = {}
|
ret = {}
|
||||||
for m in matches:
|
for match in matches:
|
||||||
for k,v in m.iteritems():
|
for key in match['config']:
|
||||||
if not k in ret:
|
value = match['config'][key]
|
||||||
ret[k] = v
|
if key == 'identityfile':
|
||||||
|
if key in ret:
|
||||||
|
ret['identityfile'].extend(value)
|
||||||
|
else:
|
||||||
|
ret['identityfile'] = value
|
||||||
|
elif key not in ret:
|
||||||
|
ret[key] = value
|
||||||
ret = self._expand_variables(ret, hostname)
|
ret = self._expand_variables(ret, hostname)
|
||||||
del ret['host']
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def _expand_variables(self, config, hostname ):
|
def _allowed(self, hostname, hosts):
|
||||||
|
match = False
|
||||||
|
for host in hosts:
|
||||||
|
if host.startswith('!') and fnmatch.fnmatch(hostname, host[1:]):
|
||||||
|
return False
|
||||||
|
elif fnmatch.fnmatch(hostname, host):
|
||||||
|
match = True
|
||||||
|
return match
|
||||||
|
|
||||||
|
def _expand_variables(self, config, hostname):
|
||||||
"""
|
"""
|
||||||
Return a dict of config options with expanded substitutions
|
Return a dict of config options with expanded substitutions
|
||||||
for a given hostname.
|
for a given hostname.
|
||||||
|
|
||||||
Please refer to man ssh_config(5) for the parameters that
|
Please refer to man C{ssh_config} for the parameters that
|
||||||
are replaced.
|
are replaced.
|
||||||
|
|
||||||
@param config: the config for the hostname
|
@param config: the config for the hostname
|
||||||
|
@ -222,6 +238,9 @@ class SSHConfig (object):
|
||||||
for k in config:
|
for k in config:
|
||||||
if k in replacements:
|
if k in replacements:
|
||||||
for find, replace in replacements[k]:
|
for find, replace in replacements[k]:
|
||||||
if find in config[k]:
|
if isinstance(config[k],list):
|
||||||
config[k] = config[k].replace(find, str(replace))
|
for item in range(len(config[k])):
|
||||||
|
config[k][item] = config[k][item].replace(find, str(replace))
|
||||||
|
else:
|
||||||
|
config[k] = config[k].replace(find, str(replace))
|
||||||
return config
|
return config
|
||||||
|
|
Loading…
Reference in New Issue